[DevXP] Improve Linaria pre-build speed (#18382)

## Summary

This PR improves Linaria/WYW pre-build speed and continues the migration
of `twenty-ui` components away from runtime `ThemeContext` reads toward
static CSS variables and theme constants.

### Linaria/WYW profiling plugin improvements (`twenty-shared`)

- **Babel JIT warmup**: added a `buildStart` warmup step that triggers
WYW's Babel JIT compilation before the real build starts, so the first
real file doesn't pay the cold-start penalty
- **`configResolved` hook**: detects dev vs prod mode and resolves the
correct warmup file path relative to `config.root`
- **Dev-only per-file logging**: slow file warnings are now gated behind
`isDevMode`, keeping production/CI build output clean
- **`closeBundle` summary**: moved the final top-slow-files report to
`closeBundle` for accurate end-of-build reporting
- **Removed noisy progress interval logging** in favor of the warmup log
+ final summary

### Migration from `ThemeContext` to static CSS variables / constants

Across `twenty-ui`, replaced runtime `useTheme()` reads with:
- `themeCssVariables` CSS custom properties (colors, spacing)
- Hard-coded design-system constants (`ICON.size.md` → `16`,
`ICON.stroke.sm` → `1.6`) so components no longer need a React context
at render time — enabling Linaria static extraction

**Components migrated:**
- `Button`, `AnimatedButton`, `LightButton`, `LightIconButton`,
`AnimatedLightIconButton`, `ButtonIcon`, `ButtonSoon`
- `ProgressBar` (Framer Motion width animation → CSS `transition`)
- `Info`, `HorizontalSeparator`, `LinkChip`
- `MenuPicker`, `MenuItemLeftContent`, `MenuItemIconWithGripSwap`,
`NavigationBarItem`
- `JsonArrow`, `JsonNestedNode`
- `ModalHeader`

### Other
- Added `aria-valuenow` to `ProgressBar` for accessibility
- `VisibilityHidden` component updated to inline accessibility styles
This commit is contained in:
Charles Bochet
2026-03-04 17:04:16 +01:00
committed by GitHub
parent be01a85d67
commit eda905f271
55 changed files with 691 additions and 650 deletions
@@ -1,11 +1,18 @@
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { styled } from '@linaria/react';
const StyledWorkflowRunStepJsonContainer = styled(WorkflowStepBody)`
grid-template-rows: max-content;
gap: 0;
display: grid;
overflow: auto;
`;
const WorkflowRunStepJsonContainerInner = ({
children,
}: {
children: React.ReactNode;
}) => (
<WorkflowStepBody
display="grid"
gridTemplateRows="max-content"
rowGap="0"
overflow="auto"
>
{children}
</WorkflowStepBody>
);
export { StyledWorkflowRunStepJsonContainer as WorkflowRunStepJsonContainer };
export { WorkflowRunStepJsonContainerInner as WorkflowRunStepJsonContainer };
@@ -4,27 +4,54 @@ import { type AppErrorDisplayProps } from '@/error-handler/types/AppErrorDisplay
import { styled } from '@linaria/react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledWorkflowStepBody = styled.div`
const StyledWorkflowStepBody = styled.div<{
rowGap?: string;
display?: string;
overflow?: string;
paddingBlock?: string;
paddingInline?: string;
gridTemplateRows?: string;
}>`
background: ${themeCssVariables.background.primary};
display: flex;
display: ${({ display }) => display ?? 'flex'};
flex: 1 1 auto;
flex-direction: column;
height: 100%;
overflow-y: scroll;
padding-block: ${themeCssVariables.spacing[4]};
padding-inline: ${themeCssVariables.spacing[3]};
row-gap: ${themeCssVariables.spacing[4]};
overflow: ${({ overflow }) => overflow ?? 'hidden scroll'};
padding-block: ${({ paddingBlock }) =>
paddingBlock ?? themeCssVariables.spacing[4]};
padding-inline: ${({ paddingInline }) =>
paddingInline ?? themeCssVariables.spacing[3]};
row-gap: ${({ rowGap }) => rowGap ?? themeCssVariables.spacing[4]};
grid-template-rows: ${({ gridTemplateRows }) => gridTemplateRows ?? 'none'};
`;
export const WorkflowStepBody = ({
children,
className,
rowGap,
display,
overflow,
paddingBlock,
paddingInline,
gridTemplateRows,
}: {
children: React.ReactNode;
className?: string;
rowGap?: string;
display?: string;
overflow?: string;
paddingBlock?: string;
paddingInline?: string;
gridTemplateRows?: string;
}) => {
return (
<StyledWorkflowStepBody className={className}>
<StyledWorkflowStepBody
rowGap={rowGap}
display={display}
overflow={overflow}
paddingBlock={paddingBlock}
paddingInline={paddingInline}
gridTemplateRows={gridTemplateRows}
>
<AppErrorBoundary
resetOnLocationChange={true}
FallbackComponent={({
@@ -55,11 +55,6 @@ const StyledTabList = styled(TabList)`
padding-left: ${themeCssVariables.spacing[2]};
`;
const StyledPermissionsStepBody = styled(WorkflowStepBody)`
padding-block: 0;
padding-inline: 0;
`;
export const WorkflowEditActionAiAgent = ({
action,
actionOptions,
@@ -281,14 +276,14 @@ export const WorkflowEditActionAiAgent = ({
behaveAsLinks={false}
/>
{currentTabId === WORKFLOW_AI_AGENT_TABS.PERMISSIONS ? (
<StyledPermissionsStepBody>
<WorkflowStepBody paddingBlock="0" paddingInline="0">
<WorkflowAiAgentPermissionsTab
action={action}
readonly={actionOptions.readonly === true}
isAgentLoading={agentLoading}
refetchAgent={refetchAgent}
/>
</StyledPermissionsStepBody>
</WorkflowStepBody>
) : (
<WorkflowStepBody>
<WorkflowAiAgentPromptTab
@@ -30,10 +30,6 @@ const StyledChildContainer = styled.div`
width: 100%;
`;
const StyledFilterBodyContainer = styled(WorkflowStepBody)`
gap: ${themeCssVariables.spacing[0]};
`;
type WorkflowEditActionFilterBodyProps = {
action: WorkflowFilterAction;
actionOptions:
@@ -84,7 +80,7 @@ export const WorkflowEditActionFilterBody = ({
onFilterSettingsUpdate,
}}
>
<StyledFilterBodyContainer>
<WorkflowStepBody rowGap={themeCssVariables.spacing[0]}>
<InputLabel>{t`Conditions`}</InputLabel>
{isDefined(rootStepFilterGroup) ? (
<StyledContainer>
@@ -119,7 +115,7 @@ export const WorkflowEditActionFilterBody = ({
) : (
<WorkflowStepFilterAddRootStepFilterButton />
)}
</StyledFilterBodyContainer>
</WorkflowStepBody>
</WorkflowStepFilterContext.Provider>
);
};
@@ -18,7 +18,7 @@ import { styled } from '@linaria/react';
import { type OnDragEndResponder } from '@hello-pangea/dnd';
import { useLingui } from '@lingui/react/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { useContext, useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import {
@@ -32,8 +32,7 @@ import {
import { LightIconButton } from 'twenty-ui/input';
import { useDebouncedCallback } from 'use-debounce';
import { v4 } from 'uuid';
import { ThemeContext } from 'twenty-ui/theme';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { ICON_SIZES, themeCssVariables } from 'twenty-ui/theme-constants';
export type WorkflowEditActionFormBuilderProps = {
triggerType: WorkflowTriggerType | undefined;
@@ -50,12 +49,6 @@ export type WorkflowEditActionFormBuilderProps = {
type FormData = WorkflowFormActionField[];
const StyledWorkflowStepBody = styled(WorkflowStepBody)`
display: block;
padding-left: ${themeCssVariables.spacing[2]};
padding-right: ${themeCssVariables.spacing[2]};
`;
const StyledFormFieldContainer = styled.div`
align-items: flex-end;
column-gap: ${themeCssVariables.spacing[1]};
@@ -74,17 +67,21 @@ const StyledDraggingIndicator = styled.div`
background-color: ${themeCssVariables.background.transparent.light};
`;
const StyledLightGripIconButton = styled(LightIconButton)`
const StyledGripButtonContainer = styled.div`
align-items: flex-end;
display: flex;
grid-area: grip;
margin-bottom: ${themeCssVariables.spacing[1]};
`;
const StyledLightTrashIconButton = styled(LightIconButton)`
const StyledTrashButtonContainer = styled.div`
align-items: flex-end;
display: flex;
grid-area: delete;
margin-bottom: ${themeCssVariables.spacing[1]};
`;
const StyledFormFieldInputContainer = styled(FormFieldInputContainer)`
const StyledFormFieldInputContainerWrapper = styled.div`
grid-area: input;
`;
@@ -115,7 +112,7 @@ const StyledFieldContainer = styled.div<{
}
`;
const StyledPlaceholder = styled(FormFieldPlaceholder)`
const StyledPlaceholderContainer = styled.div`
width: 100%;
`;
@@ -151,7 +148,6 @@ export const WorkflowEditActionFormBuilder = ({
action,
actionOptions,
}: WorkflowEditActionFormBuilderProps) => {
const { theme } = useContext(ThemeContext);
const { t } = useLingui();
const [formData, setFormData] = useState<FormData>(action.settings.input);
@@ -233,7 +229,10 @@ export const WorkflowEditActionFormBuilder = ({
return (
<>
<StyledWorkflowStepBody>
<WorkflowStepBody
display="block"
paddingInline={themeCssVariables.spacing[2]}
>
{triggerType && triggerType !== 'MANUAL' && isCalloutVisible && (
<StyledCalloutContainer>
<Callout
@@ -296,13 +295,15 @@ export const WorkflowEditActionFormBuilder = ({
{isDragging && <StyledDraggingIndicator />}
{showButtons && (
<StyledLightGripIconButton
Icon={IconGripVertical}
aria-label={t`Reorder field`}
/>
<StyledGripButtonContainer>
<LightIconButton
Icon={IconGripVertical}
aria-label={t`Reorder field`}
/>
</StyledGripButtonContainer>
)}
<StyledFormFieldInputContainer>
<StyledFormFieldInputContainerWrapper>
<InputLabel>{field.label || ''}</InputLabel>
<FormFieldInputRowContainer>
@@ -316,44 +317,51 @@ export const WorkflowEditActionFormBuilder = ({
<StyledFieldContainer
readonly={actionOptions.readonly}
>
<StyledPlaceholder>
{isDefined(field.placeholder) &&
isNonEmptyString(field.placeholder)
? field.placeholder
: getDefaultFormFieldSettings(field.type)
.placeholder}
</StyledPlaceholder>
<StyledPlaceholderContainer>
<FormFieldPlaceholder>
{isDefined(field.placeholder) &&
isNonEmptyString(field.placeholder)
? field.placeholder
: getDefaultFormFieldSettings(field.type)
.placeholder}
</FormFieldPlaceholder>
</StyledPlaceholderContainer>
{field.type === 'RECORD' && (
<IconChevronDown
size={theme.icon.size.md}
color={theme.font.color.tertiary}
size={ICON_SIZES.md}
color={
themeCssVariables.font.color.tertiary
}
/>
)}
</StyledFieldContainer>
</FormFieldInputInnerContainer>
</FormFieldInputRowContainer>
</StyledFormFieldInputContainer>
</StyledFormFieldInputContainerWrapper>
{showButtons && (
<StyledLightTrashIconButton
Icon={IconTrash}
aria-label={t`Delete field`}
onClick={() => {
const updatedFormData = formData.filter(
(currentField) => currentField.id !== field.id,
);
<StyledTrashButtonContainer>
<LightIconButton
Icon={IconTrash}
aria-label={t`Delete field`}
onClick={() => {
const updatedFormData = formData.filter(
(currentField) =>
currentField.id !== field.id,
);
setFormData(updatedFormData);
setFormData(updatedFormData);
actionOptions.onActionUpdate({
...action,
settings: {
...action.settings,
input: updatedFormData,
},
});
}}
/>
actionOptions.onActionUpdate({
...action,
settings: {
...action.settings,
input: updatedFormData,
},
});
}}
/>
</StyledTrashButtonContainer>
)}
{isFieldSelected(field.id) && (
@@ -410,7 +418,7 @@ export const WorkflowEditActionFormBuilder = ({
>
<StyledFieldContainer>
<StyledAddFieldButtonContentContainer>
<IconPlus size={theme.icon.size.sm} />
<IconPlus size={ICON_SIZES.sm} />
{t`Add Field`}
</StyledAddFieldButtonContentContainer>
</StyledFieldContainer>
@@ -419,7 +427,7 @@ export const WorkflowEditActionFormBuilder = ({
</FormFieldInputContainer>
</StyledAddFieldButtonContainer>
)}
</StyledWorkflowStepBody>
</WorkflowStepBody>
{!actionOptions.readonly && <WorkflowStepFooter stepId={action.id} />}
</>
);
@@ -38,10 +38,6 @@ const StyledContainer = styled.div`
gap: ${themeCssVariables.spacing[4]};
`;
const StyledBodyContainer = styled(WorkflowStepBody)`
gap: ${themeCssVariables.spacing[2]};
`;
type WorkflowEditActionIfElseBodyProps = {
action: WorkflowIfElseAction;
actionOptions:
@@ -250,7 +246,7 @@ export const WorkflowEditActionIfElseBody = ({
};
return (
<StyledBodyContainer>
<WorkflowStepBody rowGap={themeCssVariables.spacing[2]}>
<InputLabel>{t`Conditions`}</InputLabel>
<StyledContainer>
{branches.map((branch, branchIndex) => {
@@ -303,6 +299,6 @@ export const WorkflowEditActionIfElseBody = ({
);
})}
</StyledContainer>
</StyledBodyContainer>
</WorkflowStepBody>
);
};