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
@@ -2,13 +2,14 @@ import { styled } from '@linaria/react';
import { IconButtonGroup, type IconButtonGroupProps } from 'twenty-ui/input';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledIconButtonGroup = styled(IconButtonGroup)`
const StyledIconButtonGroupContainer = styled.div`
pointer-events: all;
`;
const StyledSelectedIconButtonGroup = styled(StyledIconButtonGroup)`
const StyledSelectedIconButtonGroupContainer = styled.div`
background-color: ${themeCssVariables.color.blue2};
border-color: ${themeCssVariables.color.blue};
pointer-events: all;
`;
type WorkflowDiagramEdgeButtonGroupProps = IconButtonGroupProps & {
@@ -19,9 +20,13 @@ export const WorkflowDiagramEdgeButtonGroup = ({
selected = false,
iconButtons,
}: WorkflowDiagramEdgeButtonGroupProps) => {
const ButtonGroup = selected
? StyledSelectedIconButtonGroup
: StyledIconButtonGroup;
const Container = selected
? StyledSelectedIconButtonGroupContainer
: StyledIconButtonGroupContainer;
return <ButtonGroup className="nodrag nopan" iconButtons={iconButtons} />;
return (
<Container>
<IconButtonGroup className="nodrag nopan" iconButtons={iconButtons} />
</Container>
);
};
@@ -9,8 +9,8 @@ import { themeCssVariables } from 'twenty-ui/theme-constants';
const HANDLE_SCALE_ON_HOVER = 1.5;
const StyledHandle = styled(Handle)`
&.react-flow__handle {
const StyledHandleContainer = styled.div`
& .react-flow__handle {
height: ${NODE_HANDLE_HEIGHT_PX}px;
width: ${NODE_HANDLE_WIDTH_PX}px;
opacity: var(--handle-opacity, 1);
@@ -110,11 +110,8 @@ export const WorkflowDiagramHandleSource = ({
}, [position, selected, hovered, disableHoverEffect, runStatus, type]);
return (
<StyledHandle
id={id}
type={type}
position={position}
style={dynamicStyles}
/>
<StyledHandleContainer>
<Handle id={id} type={type} position={position} style={dynamicStyles} />
</StyledHandleContainer>
);
};
@@ -7,8 +7,8 @@ type WorkflowDiagramHandleTargetProps = {
isConnectable?: boolean;
};
const StyledHandle = styled(Handle)`
&.react-flow__handle {
const StyledHandleContainer = styled.div`
& .react-flow__handle {
opacity: 0;
z-index: 1;
border-radius: ${themeCssVariables.border.radius.md};
@@ -28,12 +28,14 @@ export const WorkflowDiagramHandleTarget = ({
isConnectable = false,
}: WorkflowDiagramHandleTargetProps) => {
return (
<StyledHandle
id={WORKFLOW_DIAGRAM_NODE_DEFAULT_TARGET_HANDLE_ID}
type="target"
position={Position.Top}
isConnectableEnd={isConnectable}
isConnectableStart={false}
/>
<StyledHandleContainer>
<Handle
id={WORKFLOW_DIAGRAM_NODE_DEFAULT_TARGET_HANDLE_ID}
type="target"
position={Position.Top}
isConnectableEnd={isConnectable}
isConnectableStart={false}
/>
</StyledHandleContainer>
);
};
@@ -18,7 +18,6 @@ import { WorkflowDiagramStepNodeIcon } from '@/workflow/workflow-diagram/workflo
import { WorkflowNodeContainer } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeContainer';
import { WorkflowNodeIconContainer } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeIconContainer';
import { WorkflowNodeLabel } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeLabel';
import { WorkflowNodeLabelWithCounterPart } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeLabelWithCounterPart';
import { WorkflowNodeRightPart } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeRightPart';
import { WorkflowNodeTitle } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeTitle';
import { WORKFLOW_DIAGRAM_NODE_DEFAULT_SOURCE_HANDLE_ID } from '@/workflow/workflow-diagram/workflow-nodes/constants/WorkflowDiagramNodeDefaultSourceHandleId';
@@ -33,7 +32,13 @@ import { IconCheck, IconX, useIcons } from 'twenty-ui/display';
import { Loader } from 'twenty-ui/feedback';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledNodeLabelWithCounterPart = styled(WorkflowNodeLabelWithCounterPart)`
const StyledNodeLabelWithCounterPart = styled.div`
align-items: center;
align-self: stretch;
display: flex;
height: 14px;
justify-content: space-between;
box-sizing: border-box;
column-gap: ${themeCssVariables.spacing[2]};
`;
@@ -7,7 +7,7 @@ import { IconFilter } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledButton = styled(Button)`
const StyledButtonContainer = styled.div`
margin-top: ${themeCssVariables.spacing[2]};
`;
@@ -17,15 +17,17 @@ export const WorkflowStepFilterAddRootStepFilterButton = () => {
const { addRootStepFilter } = useAddRootStepFilter();
return (
<StyledButton
Icon={IconFilter}
size="small"
variant="secondary"
accent="default"
onClick={addRootStepFilter}
ariaLabel={t`Add first filter`}
title={t`Add first filter`}
disabled={readonly}
/>
<StyledButtonContainer>
<Button
Icon={IconFilter}
size="small"
variant="secondary"
accent="default"
onClick={addRootStepFilter}
ariaLabel={t`Add first filter`}
title={t`Add first filter`}
disabled={readonly}
/>
</StyledButtonContainer>
);
};
@@ -1,4 +1,5 @@
import { styled } from '@linaria/react';
import React from 'react';
import { IconChevronRight, Label } from 'twenty-ui/display';
import { themeCssVariables } from 'twenty-ui/theme-constants';
@@ -6,11 +7,17 @@ export const StyledText = styled.div`
color: ${themeCssVariables.font.color.secondary};
`;
export const StyledLabel = styled(Label)`
const StyledLabelWrapper = styled.div`
margin: ${themeCssVariables.spacing[3]} ${themeCssVariables.spacing[3]}
${themeCssVariables.spacing[0]};
`;
export const StyledLabel = ({ children }: { children: React.ReactNode }) => (
<StyledLabelWrapper>
<Label>{children}</Label>
</StyledLabelWrapper>
);
export const StyledRow = styled.div<{ isDisabled?: boolean }>`
align-items: center;
display: flex;
@@ -61,6 +68,18 @@ export const StyledIconContainer = styled.div`
color: ${themeCssVariables.font.color.tertiary};
`;
export const StyledIconChevronRight = styled(IconChevronRight)`
const StyledIconChevronRightWrapper = styled.div`
align-items: center;
color: ${themeCssVariables.font.color.tertiary};
display: inline-flex;
`;
export const StyledIconChevronRight = ({
size,
color,
stroke,
}: React.ComponentProps<typeof IconChevronRight>) => (
<StyledIconChevronRightWrapper>
<IconChevronRight size={size} color={color} stroke={stroke} />
</StyledIconChevronRightWrapper>
);
@@ -26,11 +26,11 @@ import { WorkflowAiAgentPermissionsFlagList } from './WorkflowAiAgentPermissions
import { WorkflowAiAgentPermissionsObjectsList } from './WorkflowAiAgentPermissionsObjectsList';
import { getFilteredPermissions } from './workflowAiAgentPermissions.utils';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledSearchInput = styled(TextInput)`
const StyledSearchInputContainer = styled.div`
width: 100%;
height: 40px;
border-block: 1px solid ${themeCssVariables.border.color.medium};
input {
& input {
height: 40px;
line-height: 40px;
border: none;
@@ -198,16 +198,18 @@ export const WorkflowAiAgentPermissionsTab = ({
</StyledBackButton>
)}
<StyledSearchInput
value={searchQuery}
onChange={(value: string) => setSearchQuery(value)}
placeholder={t`Type anything...`}
onKeyDown={(event) => {
if (isNonTextWritingKey(event.key)) {
event.stopPropagation();
}
}}
/>
<StyledSearchInputContainer>
<TextInput
value={searchQuery}
onChange={(value: string) => setSearchQuery(value)}
placeholder={t`Type anything...`}
onKeyDown={(event) => {
if (isNonTextWritingKey(event.key)) {
event.stopPropagation();
}
}}
/>
</StyledSearchInputContainer>
{shouldShowCrudList && (
<WorkflowAiAgentPermissionsCrudList
@@ -50,7 +50,7 @@ type WorkflowEditActionAiAgentProps = {
};
};
const StyledTabList = styled(TabList)`
const StyledTabListContainer = styled.div`
background-color: ${themeCssVariables.background.secondary};
padding-left: ${themeCssVariables.spacing[2]};
`;
@@ -270,11 +270,13 @@ export const WorkflowEditActionAiAgent = ({
<SidePanelSkeletonLoader />
) : (
<>
<StyledTabList
tabs={tabs}
componentInstanceId={componentInstanceId}
behaveAsLinks={false}
/>
<StyledTabListContainer>
<TabList
tabs={tabs}
componentInstanceId={componentInstanceId}
behaveAsLinks={false}
/>
</StyledTabListContainer>
{currentTabId === WORKFLOW_AI_AGENT_TABS.PERMISSIONS ? (
<WorkflowStepBody paddingBlock="0" paddingInline="0">
<WorkflowAiAgentPermissionsTab
@@ -55,7 +55,7 @@ const StyledCodeEditorContainer = styled.div`
position: relative;
`;
const StyledTabList = styled(TabList)`
const StyledTabListContainer = styled.div`
background-color: ${themeCssVariables.background.secondary};
padding-left: ${themeCssVariables.spacing[2]};
`;
@@ -370,11 +370,13 @@ export const WorkflowEditActionCode = ({
!loading && (
<>
<LogicFunctionTestInputInitEffect logicFunctionId={logicFunctionId} />
<StyledTabList
tabs={tabs}
behaveAsLinks={false}
componentInstanceId={WORKFLOW_LOGIC_FUNCTION_TAB_LIST_COMPONENT_ID}
/>
<StyledTabListContainer>
<TabList
tabs={tabs}
behaveAsLinks={false}
componentInstanceId={WORKFLOW_LOGIC_FUNCTION_TAB_LIST_COMPONENT_ID}
/>
</StyledTabListContainer>
<WorkflowStepBody>
{activeTabId === WorkflowLogicFunctionTabId.CODE && (
<>
@@ -139,8 +139,11 @@ const StyledCalloutContainer = styled.div`
padding-top: ${themeCssVariables.spacing[2]};
`;
const StyledNotClosableCalloutContainer = styled(StyledCalloutContainer)`
const StyledNotClosableCalloutContainer = styled.div`
padding-bottom: ${themeCssVariables.spacing[4]};
padding-left: ${themeCssVariables.spacing[7]};
padding-right: ${themeCssVariables.spacing[7]};
padding-top: ${themeCssVariables.spacing[2]};
`;
export const WorkflowEditActionFormBuilder = ({
@@ -32,7 +32,7 @@ const StyledContainer = styled.div`
gap: ${themeCssVariables.spacing[2]};
`;
const StyledSelectDropdown = styled(Select)`
const StyledSelectDropdownContainer = styled.div`
margin-bottom: ${themeCssVariables.spacing[2]};
`;
const StyledNoBodyMessage = styled.div`
@@ -163,23 +163,25 @@ export const BodyInput = ({
return (
<FormFieldInputContainer>
<InputLabel>{t`Body Input`}</InputLabel>
<StyledSelectDropdown
options={[
{ label: t`Key/Value`, value: BODY_TYPES.KEY_VALUE, Icon: IconKey },
{
label: t`Raw JSON`,
value: BODY_TYPES.RAW_JSON,
Icon: IconFileText,
},
{ label: t`Form Data`, value: BODY_TYPES.FORM_DATA, Icon: IconKey },
{ label: t`Text`, value: BODY_TYPES.TEXT, Icon: IconFileText },
{ label: t`None`, value: BODY_TYPES.NONE, Icon: IconFileText },
]}
dropdownId="body-input-mode"
value={getBodyTypeFromHeaders(headers) || BODY_TYPES.NONE}
onChange={(value) => handleModeChange(value as BodyType)}
disabled={readonly}
/>
<StyledSelectDropdownContainer>
<Select
options={[
{ label: t`Key/Value`, value: BODY_TYPES.KEY_VALUE, Icon: IconKey },
{
label: t`Raw JSON`,
value: BODY_TYPES.RAW_JSON,
Icon: IconFileText,
},
{ label: t`Form Data`, value: BODY_TYPES.FORM_DATA, Icon: IconKey },
{ label: t`Text`, value: BODY_TYPES.TEXT, Icon: IconFileText },
{ label: t`None`, value: BODY_TYPES.NONE, Icon: IconFileText },
]}
dropdownId="body-input-mode"
value={getBodyTypeFromHeaders(headers) || BODY_TYPES.NONE}
onChange={(value) => handleModeChange(value as BodyType)}
disabled={readonly}
/>
</StyledSelectDropdownContainer>
<StyledContainer>
{isBodyTypeRawJson ? (
@@ -39,7 +39,7 @@ type WorkflowEditActionHttpRequestProps = {
};
};
const StyledTabList = styled(TabList)`
const StyledTabListContainer = styled.div`
background-color: ${themeCssVariables.background.secondary};
padding-left: ${themeCssVariables.spacing[2]};
`;
@@ -61,7 +61,7 @@ const StyledConfigurationTabContent = styled.div`
flex: 1;
`;
const StyledFullHeightFormRawJsonFieldInput = styled(FormRawJsonFieldInput)`
const StyledFullHeightFormRawJsonFieldInputContainer = styled.div`
flex: 1;
display: flex;
flex-direction: column;
@@ -128,11 +128,13 @@ export const WorkflowEditActionHttpRequest = ({
return (
<>
<StyledTabList
tabs={tabs}
behaveAsLinks={false}
componentInstanceId={WORKFLOW_HTTP_REQUEST_TAB_LIST_COMPONENT_ID}
/>
<StyledTabListContainer>
<TabList
tabs={tabs}
behaveAsLinks={false}
componentInstanceId={WORKFLOW_HTTP_REQUEST_TAB_LIST_COMPONENT_ID}
/>
</StyledTabListContainer>
<WorkflowStepBody>
{activeTabId === WorkflowHttpRequestTabId.CONFIGURATION && (
<StyledConfigurationTabContent>
@@ -178,14 +180,16 @@ export const WorkflowEditActionHttpRequest = ({
/>
)}
<StyledFullHeightFormRawJsonFieldInput
label={t`Expected Response Body`}
placeholder={JSON_RESPONSE_PLACEHOLDER}
defaultValue={outputSchema}
onChange={handleOutputSchemaChange}
readonly={actionOptions.readonly}
error={error}
/>
<StyledFullHeightFormRawJsonFieldInputContainer>
<FormRawJsonFieldInput
label={t`Expected Response Body`}
placeholder={JSON_RESPONSE_PLACEHOLDER}
defaultValue={outputSchema}
onChange={handleOutputSchemaChange}
readonly={actionOptions.readonly}
error={error}
/>
</StyledFullHeightFormRawJsonFieldInputContainer>
</StyledConfigurationTabContent>
)}
{activeTabId === WorkflowHttpRequestTabId.TEST && (
@@ -1,5 +1,4 @@
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { StyledDropdownButtonContainer } from '@/ui/layout/dropdown/components/StyledDropdownButtonContainer';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
@@ -15,17 +14,29 @@ import { useContext, useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { IconVariablePlus } from 'twenty-ui/display';
import { themeCssVariables, ThemeContext } from 'twenty-ui/theme-constants';
const StyledDropdownVariableButtonContainer = styled(
StyledDropdownButtonContainer,
)<{ transparentBackground?: boolean; disabled?: boolean }>`
const StyledDropdownVariableButtonContainer = styled.div<{
isUnfolded?: boolean;
transparentBackground?: boolean;
disabled?: boolean;
}>`
align-items: center;
background-color: ${({ transparentBackground }) =>
transparentBackground
? 'transparent'
: themeCssVariables.background.transparent.lighter};
border-radius: ${themeCssVariables.border.radius.sm};
color: ${themeCssVariables.font.color.tertiary};
cursor: pointer;
display: flex;
padding: ${themeCssVariables.spacing[2]};
:hover {
user-select: none;
&:hover {
background: ${({ isUnfolded, transparentBackground }) =>
transparentBackground
? 'transparent'
: isUnfolded
? themeCssVariables.background.transparent.medium
: themeCssVariables.background.transparent.light};
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
}
`;