[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:
+15
-8
@@ -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 };
|
||||
|
||||
+36
-9
@@ -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={({
|
||||
|
||||
+2
-7
@@ -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
|
||||
|
||||
+2
-6
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+57
-49
@@ -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} />}
|
||||
</>
|
||||
);
|
||||
|
||||
+2
-6
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user