Remove all styled(Component) patterns in favor of parent wrappers and props (#18430)

## Summary

Eliminates all ~350 `styled(Component)` usages across `twenty-front` and
`twenty-ui` (212 files changed). Each was replaced following these
rules:

- **Margin/layout CSS** (margin, padding, flex, align-self, width) →
wrapped in a `styled.div`/`styled.span` parent container
- **Third-party components** (Link, TextareaAutosize,
ReactPhoneNumberInput, Handle, etc.) → parent container with child CSS
selectors (`> a`, `> textarea`, `> input`, etc.)
- **Intrinsic behavior via existing props** (TableRow
`gridTemplateColumns`, TableCell `color`/`align`) → replaced
`styled(TableRow)` / `styled(TableCell)` with direct prop usage
- **Other visual overrides on twenty-ui components** (Card, Section,
TabList, Button, MenuItem, ScrollWrapper, etc.) → parent wrappers with
`> div` / `> *` child selectors
- **Extending styled.div/span** → merged all CSS into a single
`styled.div`/`styled.span`

Also adds `overflow: hidden` to parent containers wrapping
`ScrollWrapper` so scroll activates correctly with the new wrapper
structure.

### Migration patterns

| Before | After |
|--------|-------|
| `styled(Avatar)` with `margin-right` | `<StyledAvatarContainer><Avatar
/></StyledAvatarContainer>` |
| `styled(Link)` with `text-decoration: none` |
`<StyledLinkContainer><Link /></StyledLinkContainer>` with `> a { ... }`
|
| `styled(TableRow)` with `grid-template-columns` | `<TableRow
gridTemplateColumns="..." />` |
| `styled(TableCell)` with `color` / `align` | `<TableCell color={...}
align="right" />` |
| `styled(Card)` with `margin-top` | `<StyledCardContainer><Card
/></StyledCardContainer>` |
| `styled(TabList)` with `background` |
`<StyledTabListContainer><TabList /></StyledTabListContainer>` with `>
div { ... }` |
| `styled(StyledBase)` extending a `styled.div` | Single merged
`styled.div` with all styles inlined |
This commit is contained in:
Charles Bochet
2026-03-05 18:16:25 +01:00
committed by GitHub
parent e5e3132ddd
commit c53a13417e
224 changed files with 4765 additions and 3837 deletions
@@ -26,8 +26,10 @@ const StyledDotContainer = styled.div<{ dotPosition: DotPosition }>`
dotPosition === 'top' ? 'stretch' : 'center'};
`;
const StyledIconPoint = styled(IconPoint)`
const StyledIconPointContainer = styled.span`
margin-right: 0;
display: flex;
align-items: center;
`;
export const AdvancedSettingsContentWrapperWithDot = ({
@@ -41,11 +43,13 @@ export const AdvancedSettingsContentWrapperWithDot = ({
<StyledWrapper>
{!hideDot && (
<StyledDotContainer dotPosition={dotPosition}>
<StyledIconPoint
size={12}
color={theme.color.yellow}
fill={theme.color.yellow}
/>
<StyledIconPointContainer>
<IconPoint
size={12}
color={theme.color.yellow}
fill={theme.color.yellow}
/>
</StyledIconPointContainer>
</StyledDotContainer>
)}
{children}
@@ -18,28 +18,34 @@ type SettingsCardProps = {
Status?: ReactNode;
};
const StyledCard = styled(Card)<{
const StyledCardWrapper = styled.div<{
disabled?: boolean;
onClick?: () => void;
clickable?: boolean;
}>`
color: ${({ disabled }) =>
disabled
? themeCssVariables.font.color.extraLight
: themeCssVariables.font.color.tertiary};
cursor: ${({ disabled, onClick }) =>
disabled ? 'not-allowed' : onClick ? 'pointer' : 'default'};
cursor: ${({ disabled, clickable }) =>
disabled ? 'not-allowed' : clickable ? 'pointer' : 'default'};
width: 100%;
> * {
color: inherit;
}
`;
const StyledCardContent = styled(CardContent)`
display: flex;
flex-direction: column;
gap: ${themeCssVariables.spacing[2]};
padding: ${themeCssVariables.spacing[2]} ${themeCssVariables.spacing[2]};
const StyledCardContentContainer = styled.div`
> * {
display: flex;
flex-direction: column;
gap: ${themeCssVariables.spacing[2]};
padding: ${themeCssVariables.spacing[2]} ${themeCssVariables.spacing[2]};
&:hover {
background-color: ${themeCssVariables.background.quaternary};
cursor: pointer;
&:hover {
background-color: ${themeCssVariables.background.quaternary};
cursor: pointer;
}
}
`;
@@ -61,8 +67,10 @@ const StyledTitle = styled.div<{ disabled?: boolean }>`
justify-content: flex-start;
`;
const StyledIconChevronRight = styled(IconChevronRight)`
const StyledIconChevronRightContainer = styled.span`
align-items: center;
color: ${themeCssVariables.font.color.light};
display: flex;
`;
const StyledDescription = styled.div`
@@ -91,24 +99,31 @@ export const SettingsCard = ({
const { theme } = useContext(ThemeContext);
return (
<StyledCard
<StyledCardWrapper
disabled={disabled}
onClick={disabled ? undefined : onClick}
clickable={!!onClick}
className={className}
rounded={true}
>
<StyledCardContent>
<StyledHeader>
<StyledIconContainer>{Icon}</StyledIconContainer>
<StyledTitle disabled={disabled}>
{title}
{soon && <Pill label={t`Soon`} />}
</StyledTitle>
{Status && Status}
<StyledIconChevronRight size={theme.icon.size.sm} />
</StyledHeader>
{description && <StyledDescription>{description}</StyledDescription>}
</StyledCardContent>
</StyledCard>
<Card onClick={disabled ? undefined : onClick} rounded={true} fullWidth>
<StyledCardContentContainer>
<CardContent>
<StyledHeader>
<StyledIconContainer>{Icon}</StyledIconContainer>
<StyledTitle disabled={disabled}>
{title}
{soon && <Pill label={t`Soon`} />}
</StyledTitle>
{Status && Status}
<StyledIconChevronRightContainer>
<IconChevronRight size={theme.icon.size.sm} />
</StyledIconChevronRightContainer>
</StyledHeader>
{description && (
<StyledDescription>{description}</StyledDescription>
)}
</CardContent>
</StyledCardContentContainer>
</Card>
</StyledCardWrapper>
);
};
@@ -14,10 +14,12 @@ const StyledNameCell = styled.div`
gap: ${themeCssVariables.spacing[1]};
`;
const StyledTableRow = styled(TableRow)`
&:hover {
background: ${themeCssVariables.background.transparent.light};
cursor: pointer;
const StyledTableRowContainer = styled.div`
> * {
&:hover {
background-color: ${themeCssVariables.background.transparent.light};
cursor: pointer;
}
}
`;
@@ -32,19 +34,23 @@ export const SettingsConnectedAccountsTableRow = ({
const IconComponent = SettingsConnectedAccountIcon({ account });
return (
<StyledTableRow key={account.id} gridAutoColumns="332px 1fr">
<TableCell>
<StyledNameCell>
<IconComponent
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
<StyledTableRowContainer>
<TableRow key={account.id} gridAutoColumns="332px 1fr">
<TableCell>
<StyledNameCell>
<IconComponent
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
/>
{account.handle}
</StyledNameCell>
</TableCell>
<TableCell align="right">
<SettingsAccountsConnectedAccountsRowRightContainer
account={account}
/>
{account.handle}
</StyledNameCell>
</TableCell>
<TableCell align="right">
<SettingsAccountsConnectedAccountsRowRightContainer account={account} />
</TableCell>
</StyledTableRow>
</TableCell>
</TableRow>
</StyledTableRowContainer>
);
};
@@ -25,9 +25,10 @@ const StyledCounterContainer = styled.div<{ showButtons: boolean }>`
: themeCssVariables.spacing[16]};
`;
const StyledTextInput = styled(SettingsTextInput)`
const StyledTextInputContainer = styled.div`
width: ${themeCssVariables.spacing[16]};
input {
> * input {
width: ${themeCssVariables.spacing[16]};
height: ${themeCssVariables.spacing[6]};
text-align: center;
@@ -84,14 +85,16 @@ export const SettingsCounter = ({
disabled={disabled}
/>
)}
<StyledTextInput
instanceId="settings-counter-input"
name="counter"
fullWidth
value={value.toString()}
onChange={handleTextInputChange}
disabled={disabled}
/>
<StyledTextInputContainer>
<SettingsTextInput
instanceId="settings-counter-input"
name="counter"
fullWidth
value={value.toString()}
onChange={handleTextInputChange}
disabled={disabled}
/>
</StyledTextInputContainer>
{showButtons && (
<IconButton
size="small"
@@ -28,15 +28,16 @@ type SettingsDnsRecordsTableProps = {
records: DnsRecord[];
};
const StyledTableRow = styled(TableRow)`
& > * {
const StyledTableRowContainer = styled.div`
> * > * {
min-width: 0;
max-width: 100%;
overflow: hidden;
}
`;
const StyledTableCell = styled(TableCell)`
const StyledTableCellFontWrapper = styled.div`
display: contents;
font-family: monospace;
`;
@@ -69,53 +70,69 @@ export const SettingsDnsRecordsTable = ({
return (
<Table>
<StyledTableRow gridAutoColumns={gridAutoColumns}>
<TableHeader align="center">{t`Type`}</TableHeader>
<TableHeader align="center">{t`Key`}</TableHeader>
<TableHeader align="center">{t`Value`}</TableHeader>
{hasPriorityRecords && (
<TableHeader align="center">{t`Priority`}</TableHeader>
)}
{hasTtlRecords && <TableHeader align="center">{t`TTL`}</TableHeader>}
{hasStatusRecords && (
<TableHeader align="center">{t`Status`}</TableHeader>
)}
</StyledTableRow>
<StyledTableRowContainer>
<TableRow gridAutoColumns={gridAutoColumns}>
<TableHeader align="center">{t`Type`}</TableHeader>
<TableHeader align="center">{t`Key`}</TableHeader>
<TableHeader align="center">{t`Value`}</TableHeader>
{hasPriorityRecords && (
<TableHeader align="center">{t`Priority`}</TableHeader>
)}
{hasTtlRecords && <TableHeader align="center">{t`TTL`}</TableHeader>}
{hasStatusRecords && (
<TableHeader align="center">{t`Status`}</TableHeader>
)}
</TableRow>
</StyledTableRowContainer>
{records.map((record) => (
<StyledTableRow key={record.value} gridAutoColumns={gridAutoColumns}>
<TableCell>{record.type}</TableCell>
<StyledTableCell
onClick={() => {
copyToClipboard(record.key || '');
}}
>
<OverflowingTextWithTooltip text={record.key} />
</StyledTableCell>
<StyledTableRowContainer key={record.value}>
<TableRow gridAutoColumns={gridAutoColumns}>
<TableCell>{record.type}</TableCell>
<StyledTableCellFontWrapper>
<TableCell
onClick={() => {
copyToClipboard(record.key || '');
}}
>
<OverflowingTextWithTooltip text={record.key} />
</TableCell>
</StyledTableCellFontWrapper>
<StyledTableCell
onClick={() => {
copyToClipboard(record.value);
}}
>
<OverflowingTextWithTooltip text={record.value} />
</StyledTableCell>
<StyledTableCellFontWrapper>
<TableCell
onClick={() => {
copyToClipboard(record.value);
}}
>
<OverflowingTextWithTooltip text={record.value} />
</TableCell>
</StyledTableCellFontWrapper>
{hasPriorityRecords && (
<StyledTableCell>{record.priority}</StyledTableCell>
)}
{hasTtlRecords && <StyledTableCell>{record.ttl}</StyledTableCell>}
{hasStatusRecords && (
<StyledTableCell>
{'status' in record ? (
<Status
color={record.statusColor}
text={capitalize(record.status)}
/>
) : null}
</StyledTableCell>
)}
</StyledTableRow>
{hasPriorityRecords && (
<StyledTableCellFontWrapper>
<TableCell>{record.priority}</TableCell>
</StyledTableCellFontWrapper>
)}
{hasTtlRecords && (
<StyledTableCellFontWrapper>
<TableCell>{record.ttl}</TableCell>
</StyledTableCellFontWrapper>
)}
{hasStatusRecords && (
<StyledTableCellFontWrapper>
<TableCell>
{'status' in record ? (
<Status
color={record.statusColor}
text={capitalize(record.status)}
/>
) : null}
</TableCell>
</StyledTableCellFontWrapper>
)}
</TableRow>
</StyledTableRowContainer>
))}
</Table>
);
@@ -8,10 +8,12 @@ import { Card, CardFooter } from 'twenty-ui/layout';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
import { SettingsListItemCardContent } from './SettingsListItemCardContent';
const StyledFooter = styled(CardFooter)`
align-items: center;
display: flex;
padding: ${themeCssVariables.spacing[1]};
const StyledFooterContainer = styled.div`
> * {
align-items: center;
display: flex;
padding: ${themeCssVariables.spacing[1]};
}
`;
const StyledButton = styled.button`
@@ -91,12 +93,14 @@ export const SettingsListCard = <
/>
))}
{hasFooter && (
<StyledFooter divider={!!items.length}>
<StyledButton onClick={onFooterButtonClick}>
<IconPlus size={theme.icon.size.md} />
{footerButtonLabel}
</StyledButton>
</StyledFooter>
<StyledFooterContainer>
<CardFooter divider={!!items.length}>
<StyledButton onClick={onFooterButtonClick}>
<IconPlus size={theme.icon.size.md} />
{footerButtonLabel}
</StyledButton>
</CardFooter>
</StyledFooterContainer>
)}
</Card>
);
@@ -6,15 +6,17 @@ import { IconChevronRight, type IconComponent } from 'twenty-ui/display';
import { CardContent } from 'twenty-ui/layout';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
const StyledRow = styled(CardContent)`
align-items: center;
display: flex;
font-size: ${themeCssVariables.font.size.sm};
font-weight: ${themeCssVariables.font.weight.medium};
gap: ${themeCssVariables.spacing[2]};
padding: ${themeCssVariables.spacing[2]};
padding-left: ${themeCssVariables.spacing[3]};
min-height: ${themeCssVariables.spacing[6]};
const StyledRowContainer = styled.div`
> * {
align-items: center;
display: flex;
font-size: ${themeCssVariables.font.size.sm};
font-weight: ${themeCssVariables.font.weight.medium};
gap: ${themeCssVariables.spacing[2]};
padding: ${themeCssVariables.spacing[2]};
padding-left: ${themeCssVariables.spacing[3]};
min-height: ${themeCssVariables.spacing[6]};
}
`;
const StyledRightContainer = styled.div`
@@ -43,9 +45,11 @@ const StyledDescription = styled.span`
padding-left: ${themeCssVariables.spacing[1]};
`;
const StyledLink = styled(Link)`
color: ${themeCssVariables.font.color.secondary};
text-decoration: none;
const StyledLinkContainer = styled.div`
> a {
color: ${themeCssVariables.font.color.secondary};
text-decoration: none;
}
`;
type SettingsListItemCardContentProps = {
@@ -72,36 +76,44 @@ export const SettingsListItemCardContent = ({
const { theme } = useContext(ThemeContext);
const content = (
<StyledRow
onClick={onClick}
divider={divider}
isClickable={!!onClick || !!to}
hasHoverHighlight={!!to}
>
{!!LeftIcon && (
<LeftIcon
size={theme.icon.size.md}
color={LeftIconColor ?? 'currentColor'}
/>
)}
<StyledContent>
<StyledLabel>{label}</StyledLabel>
{!!description && <StyledDescription>{description}</StyledDescription>}
</StyledContent>
<StyledRightContainer>
{rightComponent}
{!!to && (
<IconChevronRight
<StyledRowContainer>
<CardContent
onClick={onClick}
divider={divider}
isClickable={!!onClick || !!to}
hasHoverHighlight={!!to}
>
{!!LeftIcon && (
<LeftIcon
size={theme.icon.size.md}
color={theme.font.color.tertiary}
color={LeftIconColor ?? 'currentColor'}
/>
)}
</StyledRightContainer>
</StyledRow>
<StyledContent>
<StyledLabel>{label}</StyledLabel>
{!!description && (
<StyledDescription>{description}</StyledDescription>
)}
</StyledContent>
<StyledRightContainer>
{rightComponent}
{!!to && (
<IconChevronRight
size={theme.icon.size.md}
color={theme.font.color.tertiary}
/>
)}
</StyledRightContainer>
</CardContent>
</StyledRowContainer>
);
if (isDefined(to)) {
return <StyledLink to={to}>{content}</StyledLink>;
return (
<StyledLinkContainer>
<Link to={to}>{content}</Link>
</StyledLinkContainer>
);
}
return content;
@@ -2,9 +2,17 @@ import { styled } from '@linaria/react';
import { Card } from 'twenty-ui/layout';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledCard = styled(Card)`
background-color: ${themeCssVariables.background.secondary};
const StyledCardContainer = styled.div`
height: 40px;
> * {
background-color: ${themeCssVariables.background.secondary};
height: 100%;
}
`;
export { StyledCard as SettingsListSkeletonCard };
export const SettingsListSkeletonCard = () => (
<StyledCardContainer>
<Card />
</StyledCardContainer>
);
@@ -1,6 +1,5 @@
import { Separator } from '@/settings/components/Separator';
import {
StyledSettingsCardContent,
StyledSettingsCardDescription,
StyledSettingsCardIcon,
StyledSettingsCardTextContainer,
@@ -16,7 +15,12 @@ import {
import { Toggle } from 'twenty-ui/input';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
const StyledSettingsCardToggleContent = styled(StyledSettingsCardContent)`
const StyledSettingsCardToggleContent = styled.div<{ disabled?: boolean }>`
align-items: center;
display: flex;
gap: ${themeCssVariables.spacing[3]};
background-color: ${themeCssVariables.background.secondary};
padding: ${themeCssVariables.spacing[4]};
cursor: ${({ disabled }) => (disabled ? 'default' : 'pointer')};
position: relative;
pointer-events: ${({ disabled }) => (disabled ? 'none' : 'auto')};
@@ -26,7 +30,9 @@ const StyledSettingsCardToggleContent = styled(StyledSettingsCardContent)`
}
`;
const StyledSettingsCardToggleButton = styled(Toggle)`
const StyledSettingsCardToggleButtonContainer = styled.span`
align-items: center;
display: flex;
flex-shrink: 0;
margin-left: auto;
`;
@@ -84,15 +90,17 @@ export const SettingsOptionCardContentToggle = ({
</StyledSettingsCardDescription>
)}
</StyledSettingsCardTextContainer>
<StyledSettingsCardToggleButton
id={toggleId}
value={checked}
onChange={onChange}
disabled={disabled}
toggleSize="small"
color={advancedMode ? theme.color.yellow : theme.color.blue}
centered={toggleCentered}
/>
<StyledSettingsCardToggleButtonContainer>
<Toggle
id={toggleId}
value={checked}
onChange={onChange}
disabled={disabled}
toggleSize="small"
color={advancedMode ? theme.color.yellow : theme.color.blue}
centered={toggleCentered}
/>
</StyledSettingsCardToggleButtonContainer>
</StyledSettingsCardToggleContent>
{divider && <Separator />}
</>
@@ -5,22 +5,26 @@ import { type IconComponent } from 'twenty-ui/display';
import { Radio } from 'twenty-ui/input';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
const StyledRadioCardContent = styled(CardContent)`
display: flex;
align-items: center;
padding: ${themeCssVariables.spacing[2]};
border: 1px solid ${themeCssVariables.border.color.medium};
border-radius: ${themeCssVariables.border.radius.sm};
flex-grow: 1;
gap: ${themeCssVariables.spacing[2]};
cursor: pointer;
const StyledRadioCardContentContainer = styled.div`
> * {
display: flex;
align-items: center;
padding: ${themeCssVariables.spacing[2]};
border: 1px solid ${themeCssVariables.border.color.medium};
border-radius: ${themeCssVariables.border.radius.sm};
flex-grow: 1;
gap: ${themeCssVariables.spacing[2]};
cursor: pointer;
&:hover {
background: ${themeCssVariables.background.transparent.lighter};
&:hover {
background: ${themeCssVariables.background.transparent.lighter};
}
}
`;
const StyledRadio = styled(Radio)`
const StyledRadioContainer = styled.span`
align-items: center;
display: flex;
margin-left: auto;
padding: ${themeCssVariables.spacing[1]};
`;
@@ -58,13 +62,17 @@ export const SettingsRadioCard = ({
const onClick = () => handleSelect(value);
return (
<StyledRadioCardContent tabIndex={0} onClick={onClick}>
{Icon && <Icon size={theme.icon.size.xl} color={theme.color.gray10} />}
<span>
{title && <StyledTitle>{title}</StyledTitle>}
{description && <StyledDescription>{description}</StyledDescription>}
</span>
<StyledRadio value={value} checked={isSelected} />
</StyledRadioCardContent>
<StyledRadioCardContentContainer>
<CardContent tabIndex={0} onClick={onClick}>
{Icon && <Icon size={theme.icon.size.xl} color={theme.color.gray10} />}
<span>
{title && <StyledTitle>{title}</StyledTitle>}
{description && <StyledDescription>{description}</StyledDescription>}
</span>
<StyledRadioContainer>
<Radio value={value} checked={isSelected} />
</StyledRadioContainer>
</CardContent>
</StyledRadioCardContentContainer>
);
};
@@ -8,12 +8,14 @@ type SettingsSummaryCardProps = {
rightComponent: ReactNode;
};
const StyledCardContent = styled(CardContent)`
align-items: center;
display: flex;
gap: ${themeCssVariables.spacing[2]};
padding: ${themeCssVariables.spacing[2]};
min-height: ${themeCssVariables.spacing[6]};
const StyledCardContentContainer = styled.div`
> * {
align-items: center;
display: flex;
gap: ${themeCssVariables.spacing[2]};
padding: ${themeCssVariables.spacing[2]};
min-height: ${themeCssVariables.spacing[6]};
}
`;
const StyledTitle = styled.div`
@@ -29,9 +31,11 @@ export const SettingsSummaryCard = ({
rightComponent,
}: SettingsSummaryCardProps) => (
<Card>
<StyledCardContent>
<StyledTitle>{title}</StyledTitle>
{rightComponent}
</StyledCardContent>
<StyledCardContentContainer>
<CardContent>
<StyledTitle>{title}</StyledTitle>
{rightComponent}
</CardContent>
</StyledCardContentContainer>
</Card>
);