Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a18dc24
feat(CDS-2168): adopt t-shirt size prop for Button
cb-ekuersch Jul 24, 2026
15c4a2c
feat(CDS-2445): adopt t-shirt size prop for Icon Button
cb-ekuersch Jul 24, 2026
4109b23
feat(CDS-2448): adopt t-shirt size prop for Slide Button
cb-ekuersch Jul 24, 2026
eb43f87
feat(CDS-2223, CDS-2224): adopt t-shirt size prop for Chip family
cb-ekuersch Jul 24, 2026
8cf73a7
feat(CDS-2224): adopt t-shirt size prop for Select Chip (alpha)
cb-ekuersch Jul 24, 2026
31eb5c3
feat(CDS-2173): adopt t-shirt size prop for TabbedChips (alpha)
cb-ekuersch Jul 24, 2026
d0e6268
feat: add t-shirt size prop to TextInput foundation
cb-ekuersch Jul 24, 2026
298ba1b
feat(CDS-2450): adopt t-shirt size prop for Search Input
cb-ekuersch Jul 24, 2026
553d4ef
feat(CDS-2449): adopt t-shirt size prop for Select (alpha)
cb-ekuersch Jul 24, 2026
1fc6361
feat(CDS-2451): adopt t-shirt size prop for Date Picker
cb-ekuersch Jul 24, 2026
ecfe173
feat: add cross-component size composition stories
cb-ekuersch Jul 24, 2026
6a797e4
fix: align input field heights and inside-label placement across sizes
cb-ekuersch Jul 24, 2026
c856430
refactor: address PR review feedback for t-shirt sizing
cb-ekuersch Jul 24, 2026
c2da773
feat: ensure Combobox (alpha) supports t-shirt size and deprecated co…
cb-ekuersch Jul 24, 2026
2d0f995
add/update siz composition stories
cb-ekuersch Jul 27, 2026
570f833
adjust paddles size for TabbedChips sizes
cb-ekuersch Jul 27, 2026
c597753
Improve gap spacing between inside label and text input element
cb-ekuersch Jul 28, 2026
cce3fe3
improve organization/convention of web stories
cb-ekuersch Jul 28, 2026
0405317
WIP - fix some issues the agent found while building the migrator and…
cb-ekuersch Jul 28, 2026
3e7c19c
fix: remove references to deprecated compact props internally, fix ba…
cb-ekuersch Jul 29, 2026
09dcd52
address PR feedbkac
cb-ekuersch Jul 29, 2026
8b57baf
remove size examples from deprecated selectchip
cb-ekuersch Jul 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/expo-app/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,10 @@ export const routes = [
getComponent: () =>
require('@coinbase/cds-mobile/controls/__stories__/SelectOption.stories').default,
},
{
key: 'SizeComposition',
getComponent: () => require('@coinbase/cds-mobile/__stories__/SizeComposition.stories').default,
},
{
key: 'SlideButton',
getComponent: () =>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { getMediaChipSpacingProps } from '../getMediaChipSpacingProps';

describe('getMediaChipSpacingProps', () => {
const sizeDependentCompositions = [
{ name: 'label only', start: false, children: true, end: false },
{ name: 'start + label', start: true, children: true, end: false },
{ name: 'label + end', start: false, children: true, end: true },
{ name: 'start + label + end', start: true, children: true, end: true },
] as const;

describe('backward compatibility with compact', () => {
it.each(sizeDependentCompositions)(
'compact:true deep-equals size:"xs" for $name',
({ start, children, end }) => {
expect(getMediaChipSpacingProps({ compact: true, start, children, end })).toEqual(
getMediaChipSpacingProps({ size: 'xs', start, children, end }),
);
},
);

it.each(sizeDependentCompositions)(
'compact:false deep-equals size:"s" for $name',
({ start, children, end }) => {
expect(getMediaChipSpacingProps({ compact: false, start, children, end })).toEqual(
getMediaChipSpacingProps({ size: 's', start, children, end }),
);
},
);
});

describe('default resolution', () => {
it.each(sizeDependentCompositions)(
'omitting both size and compact resolves to the s output for $name',
({ start, children, end }) => {
expect(getMediaChipSpacingProps({ start, children, end })).toEqual(
getMediaChipSpacingProps({ size: 's', start, children, end }),
);
},
);
});

describe('size wins over compact', () => {
it.each(sizeDependentCompositions)(
'size:"s" + compact:true resolves to the s output for $name',
({ start, children, end }) => {
expect(
getMediaChipSpacingProps({ size: 's', compact: true, start, children, end }),
).toEqual(getMediaChipSpacingProps({ size: 's', start, children, end }));
},
);
});

describe('media-only compositions are size-agnostic', () => {
it('start only is unaffected by size', () => {
const expected = { paddingY: 1, paddingX: 1 };
expect(getMediaChipSpacingProps({ start: true, size: 'xs' })).toEqual(expected);
expect(getMediaChipSpacingProps({ start: true, size: 's' })).toEqual(expected);
});

it('start + end is unaffected by size', () => {
const expected = { paddingStart: 1, paddingY: 1, paddingEnd: 1.5, gap: 0.75 };
expect(getMediaChipSpacingProps({ start: true, end: true, size: 'xs' })).toEqual(expected);
expect(getMediaChipSpacingProps({ start: true, end: true, size: 's' })).toEqual(expected);
});
});
});
20 changes: 16 additions & 4 deletions packages/common/src/chips/getMediaChipSpacingProps.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
import type { ThemeVars } from '@coinbase/cds-common/core/theme';

export type ChipSizeCommon = 'xs' | 's';

Comment thread
cb-ekuersch marked this conversation as resolved.
type GetMediaChipSpacingPropsParams = {
start?: boolean;
end?: boolean;
children?: boolean;
/**
* Retained for backward compatibility; when `size` is omitted it is derived from `compact`.
* @deprecated Pass `size` instead. This will be removed in a future major release.
* @deprecationExpectedRemoval v10
*/
compact?: boolean;
size?: ChipSizeCommon;
};

export const getMediaChipSpacingProps = ({
compact,
size,
Comment thread
cb-ekuersch marked this conversation as resolved.
start,
end,
children,
Expand All @@ -22,9 +31,12 @@ export const getMediaChipSpacingProps = ({
paddingBottom?: ThemeVars.Space;
gap?: ThemeVars.Space;
} => {
const resolvedSize = size ?? (compact ? 'xs' : 's'); // size wins; compact is the fallback
const isXs = resolvedSize === 'xs';

if (!start && children && !end) {
// children (label) only
return compact
return isXs
? {
paddingX: 1.5,
paddingY: 0.75,
Expand All @@ -49,7 +61,7 @@ export const getMediaChipSpacingProps = ({
}
if (start && children && !end) {
// start (media) and children (label) only
return compact
return isXs
? {
paddingStart: 1,
paddingY: 0.75,
Expand All @@ -65,7 +77,7 @@ export const getMediaChipSpacingProps = ({
}
if (!start && children && end) {
// children (label) and end (icon) only
return compact
return isXs
? {
paddingStart: 1.5,
paddingY: 0.75,
Expand All @@ -81,7 +93,7 @@ export const getMediaChipSpacingProps = ({
}
if (start && children && end) {
// start (media) and children (label) and end (icon) only
return compact
return isXs
? {
paddingStart: 1,
paddingY: 0.75,
Expand Down
2 changes: 2 additions & 0 deletions packages/common/src/types/InputBaseProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export type SharedInputProps = {
/**
* Enables compact variation
* @default false
* @deprecated Use the consuming component's own sizing prop instead. This will be removed in a future major release.
* @deprecationExpectedRemoval v10
*/
compact?: boolean;
/** Short messageArea indicating purpose of input */
Expand Down
222 changes: 222 additions & 0 deletions packages/mobile/src/__stories__/SizeComposition.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
import { useMemo, useState } from 'react';
import { useWindowDimensions } from 'react-native';
import type { DateInputValidationError } from '@coinbase/cds-common/dates/DateInputValidationError';
import { sampleTabs } from '@coinbase/cds-common/internal/data/tabs';
import type { TabValue } from '@coinbase/cds-common/tabs/useTabs';
import { gutter } from '@coinbase/cds-common/tokens/sizing';

import { Combobox } from '../alpha/combobox/Combobox';
import { Select } from '../alpha/select/Select';
import { SelectChip } from '../alpha/select-chip/SelectChip';
import { TabbedChips } from '../alpha/tabbed-chips/TabbedChips';
import { Button } from '../buttons/Button';
import { IconButton } from '../buttons/IconButton';
import type { ChipSize } from '../chips/ChipProps';
import { InputChip } from '../chips/InputChip';
import { SearchInput } from '../controls/SearchInput';
import { TextInput, type TextInputSize } from '../controls/TextInput';
import { DatePicker } from '../dates/DatePicker';
import { Example, ExampleScreen } from '../examples/ExampleScreen';
import { useTheme } from '../hooks/useTheme';
import { HStack } from '../layout/HStack';
import { VStack } from '../layout/VStack';

const dateSharedProps = {
invalidDateError: 'Please enter a valid date',
disabledDateError: 'Date unavailable',
requiredError: 'This field is required',
};

const selectOptions = [
{ value: '1', label: 'Apple' },
{ value: '2', label: 'Banana' },
{ value: '3', label: 'Cherry' },
];

const chipSelectOptions = [
{ value: 'usd', label: 'USD' },
{ value: 'eur', label: 'EUR' },
{ value: 'btc', label: 'BTC' },
];

const tabbedChipTabs = sampleTabs.slice(0, 4);

/** Spacing token used for the gap between every composed control. */
const compositionGap = 1;

/**
* Minimum controls per wrapped row, so each row always pairs at least two
* controls and their natural heights can be compared against each other.
*/
const controlsPerRow = 2;

/**
* Derives control widths from the live screen width minus the horizontal
* padding `ExampleScreen` applies, so fixed-width controls always wrap into
* rows of `controlsPerRow` instead of overflowing narrow devices.
*/
const useCompositionWidths = () => {
const { width: screenWidth } = useWindowDimensions();
const theme = useTheme();

return useMemo(() => {
const contentWidth = screenWidth - theme.space[gutter] * 2;
const totalGap = theme.space[compositionGap] * (controlsPerRow - 1);

return {
contentWidth,
controlWidth: Math.floor((contentWidth - totalGap) / controlsPerRow),
};
}, [screenWidth, theme]);
};

const InputRow = ({ size }: { size?: TextInputSize }) => {
const { controlWidth } = useCompositionWidths();
const [selectValue, setSelectValue] = useState<string | null>('1');
const [comboboxValue, setComboboxValue] = useState<string | null>('1');
const [text, setText] = useState('');
const [search, setSearch] = useState('');
const [pickerDate, setPickerDate] = useState<Date | null>(null);
const [pickerError, setPickerError] = useState<DateInputValidationError | null>(null);

const selectStyle = useMemo(() => ({ width: controlWidth }), [controlWidth]);

return (
<HStack alignItems="flex-end" flexWrap="wrap" gap={compositionGap}>
<Select
label="Select"
onChange={setSelectValue}
options={selectOptions}
placeholder="Choose"
size={size}
style={selectStyle}
value={selectValue}
/>
<Combobox
label="Combobox"
onChange={setComboboxValue}
options={selectOptions}
placeholder="Search"
size={size}
style={selectStyle}
type="single"
value={comboboxValue}
/>
<TextInput
label="Text"
onChangeText={setText}
placeholder="Name"
size={size}
value={text}
width={controlWidth}
/>
<DatePicker
{...dateSharedProps}
date={pickerDate}
error={pickerError}
helperText=""
label="Date"
onChangeDate={setPickerDate}
onErrorDate={setPickerError}
openCalendarAccessibilityLabel="Open date picker calendar"
size={size}
width={controlWidth}
/>
<SearchInput
accessibilityLabel="Search"
onChangeText={setSearch}
onClear={() => setSearch('')}
placeholder="Search"
size={size}
value={search}
width={controlWidth}
/>
</HStack>
);
};

const ChipMixRow = ({ chipSize, controlSize }: { chipSize: ChipSize; controlSize: 'xs' | 's' }) => {
const { contentWidth, controlWidth } = useCompositionWidths();
const [activeTab, setActiveTab] = useState<TabValue | null>(tabbedChipTabs[0]);
const [chipSelectValue, setChipSelectValue] = useState<string | null>('usd');
const [text, setText] = useState('');
const [search, setSearch] = useState('');
const includeTextInputs = chipSize === 's';

return (
<VStack gap={compositionGap}>
{/*
* TabbedChips wraps a horizontal ScrollView, which has no intrinsic height
* as a flex-row child, so it gets its own full-width row rather than
* sitting inside the wrapping HStack below.
*/}
<TabbedChips
activeTab={activeTab}
onChange={setActiveTab}
size={chipSize}
tabs={tabbedChipTabs}
width={contentWidth}
/>
<HStack alignItems="center" flexWrap="wrap" gap={compositionGap}>
<InputChip onPress={() => {}} size={chipSize}>
USD
</InputChip>
<SelectChip
onChange={setChipSelectValue}
options={chipSelectOptions}
placeholder="Asset"
size={chipSize}
value={chipSelectValue}
/>
<Button size={controlSize}>Action</Button>
<IconButton accessibilityLabel="Settings" name="gear" size={controlSize} />
{includeTextInputs && (
<>
<TextInput
accessibilityLabel="Filter"
onChangeText={setText}
placeholder="Filter"
size="s"
value={text}
width={controlWidth}
/>
<SearchInput
accessibilityLabel="Search"
onChangeText={setSearch}
onClear={() => setSearch('')}
placeholder="Search"
size="s"
value={search}
width={controlWidth}
/>
</>
)}
</HStack>
</VStack>
);
};

/**
* Side-by-side composition of sized controls to verify natural height alignment in a row.
*/
const SizeCompositionScreen = () => (
<ExampleScreen>
<Example title='Inputs — size="s"'>
<InputRow size="s" />
</Example>
<Example title='Inputs — size="m"'>
<InputRow size="m" />
</Example>
<Example title="Inputs — default (size l)">
<InputRow />
</Example>
<Example title='Chips + small controls — size="s"'>
<ChipMixRow chipSize="s" controlSize="s" />
</Example>
<Example title='Chips + small controls — size="xs"'>
<ChipMixRow chipSize="xs" controlSize="xs" />
</Example>
</ExampleScreen>
);

export default SizeCompositionScreen;
4 changes: 3 additions & 1 deletion packages/mobile/src/alpha/combobox/Combobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,9 @@ const ComboboxBase = memo(
paddingBottom: safeBottomPadding,
}}
>
<Button compact onPress={handleClose}>
{/* Intentionally always the densest size, matching the dropdown's inline
affordances, regardless of the Combobox's own size. */}
<Button onPress={handleClose} size="s">
{closeButtonLabel}
</Button>
</StickyFooter>
Expand Down
Loading
Loading