[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
@@ -17,13 +17,8 @@ import { RecordInlineCell } from '@/object-record/record-inline-cell/components/
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { styled } from '@linaria/react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledRecordCardBodyContainer = styled(RecordCardBodyContainer)`
padding: ${themeCssVariables.spacing[1]};
`;
type RecordCalendarCardBodyProps = {
recordId: string;
isRecordReadOnly: boolean;
@@ -73,7 +68,7 @@ export const RecordCalendarCardBody = ({
};
return (
<StyledRecordCardBodyContainer>
<RecordCardBodyContainer padding={themeCssVariables.spacing[1]}>
{visibleRecordFieldsExceptLabelIdentifier.map((recordField, index) => {
const correspondingFieldDefinition =
fieldDefinitionByFieldMetadataItemId[recordField.fieldMetadataItemId];
@@ -120,6 +115,6 @@ export const RecordCalendarCardBody = ({
</StopPropagationContainer>
);
})}
</StyledRecordCardBodyContainer>
</RecordCardBodyContainer>
);
};
@@ -9,12 +9,12 @@ import { useAtomComponentFamilyState } from '@/ui/utilities/state/jotai/hooks/us
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import { styled } from '@linaria/react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
import { isDefined } from 'twenty-shared/utils';
import { ChipVariant } from 'twenty-ui/components';
import { Checkbox, CheckboxVariant } from 'twenty-ui/input';
import { isRecordCalendarCardSelectedComponentFamilyState } from '@/object-record/record-calendar/record-calendar-card/states/isRecordCalendarCardSelectedComponentFamilyState';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledCheckboxContainer = styled.div`
margin-left: auto;
@@ -27,10 +27,6 @@ const StyledRecordChipContainer = styled.div`
padding: ${themeCssVariables.spacing[1]};
`;
const StyledRecordCardHeaderContainer = styled(RecordCardHeaderContainer)`
padding: ${themeCssVariables.spacing[1]};
`;
type RecordCalendarCardHeaderProps = {
recordId: string;
};
@@ -68,7 +64,10 @@ export const RecordCalendarCardHeader = ({
}
return (
<StyledRecordCardHeaderContainer isCompact={isCompactModeActive}>
<RecordCardHeaderContainer
isCompact={isCompactModeActive}
padding={themeCssVariables.spacing[1]}
>
<StyledRecordChipContainer>
<StopPropagationContainer>
<RecordChip
@@ -93,6 +92,6 @@ export const RecordCalendarCardHeader = ({
/>
</StopPropagationContainer>
</StyledCheckboxContainer>
</StyledRecordCardHeaderContainer>
</RecordCardHeaderContainer>
);
};
@@ -1,13 +1,13 @@
import { styled } from '@linaria/react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledCardBodyContainer = styled.div`
const StyledCardBodyContainer = styled.div<{ padding?: string }>`
display: flex;
flex-direction: column;
gap: ${themeCssVariables.spacing['0.5']};
padding-bottom: ${themeCssVariables.spacing[2]};
padding-left: 10px;
padding-right: ${themeCssVariables.spacing[2]};
padding: ${({ padding }) =>
padding ??
`0 ${themeCssVariables.spacing[2]} ${themeCssVariables.spacing[2]} 10px`};
span {
align-items: center;
display: flex;
@@ -3,6 +3,7 @@ import { themeCssVariables } from 'twenty-ui/theme-constants';
export const StyledBoardCardHeaderContainer = styled.div<{
isCompact: boolean;
padding?: string;
}>`
align-items: center;
display: flex;
@@ -10,11 +11,9 @@ export const StyledBoardCardHeaderContainer = styled.div<{
justify-content: space-between;
font-weight: ${themeCssVariables.font.weight.medium};
height: 24px;
padding-bottom: ${({ isCompact }) =>
isCompact ? themeCssVariables.spacing[2] : themeCssVariables.spacing[1]};
padding-left: ${themeCssVariables.spacing[2]};
padding-right: ${themeCssVariables.spacing[2]};
padding-top: ${themeCssVariables.spacing[2]};
padding: ${({ padding, isCompact }) =>
padding ??
`${themeCssVariables.spacing[2]} ${themeCssVariables.spacing[2]} ${isCompact ? themeCssVariables.spacing[2] : themeCssVariables.spacing[1]} ${themeCssVariables.spacing[2]}`};
transition: padding ease-in-out 160ms;
img {
@@ -11,11 +11,11 @@ import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledRecordChip = styled(RecordChip)`
const StyledRecordChipContainer = styled.div`
margin: ${themeCssVariables.spacing[2]};
`;
const StyledPlaceholder = styled(FormFieldPlaceholder)`
const StyledPlaceholderContainer = styled.div`
margin: ${themeCssVariables.spacing[2]};
`;
@@ -58,12 +58,18 @@ export const FormSingleRecordFieldChip = ({
if (!!draftValue && draftValue.type === 'static' && !!selectedRecord) {
return (
<StyledRecordChip
record={selectedRecord}
objectNameSingular={objectNameSingular}
/>
<StyledRecordChipContainer>
<RecordChip
record={selectedRecord}
objectNameSingular={objectNameSingular}
/>
</StyledRecordChipContainer>
);
}
return <StyledPlaceholder>{t`Select`}</StyledPlaceholder>;
return (
<StyledPlaceholderContainer>
<FormFieldPlaceholder>{t`Select`}</FormFieldPlaceholder>
</StyledPlaceholderContainer>
);
};
@@ -1,16 +1,8 @@
import { styled } from '@linaria/react';
import { useRecordPickerGetSearchRecordAndObjectMetadataItemFromRecordId } from '@/object-record/record-picker/hooks/useRecordPickerGetSearchRecordAndObjectMetadataItemFromRecordId';
import { MultipleRecordPickerMenuItemContent } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerMenuItemContent';
import { type RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { isDefined } from 'twenty-shared/utils';
export const StyledSelectableItem = styled(SelectableListItem)`
height: 100%;
width: 100%;
`;
type MultipleRecordPickerMenuItemProps = {
recordId: string;
onChange: (morphItem: RecordPickerPickableMorphItem) => void;
@@ -1,5 +1,4 @@
import { t } from '@lingui/core/macro';
import { styled } from '@linaria/react';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getAvatarType } from '@/object-metadata/utils/getAvatarType';
@@ -20,11 +19,6 @@ import { MenuItemMultiSelectAvatar } from 'twenty-ui/navigation';
import { multipleRecordPickerSearchableObjectMetadataItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchableObjectMetadataItemsComponentState';
import { type SearchRecord } from '~/generated/graphql';
export const StyledSelectableItem = styled(SelectableListItem)`
height: 100%;
width: 100%;
`;
type MultipleRecordPickerMenuItemContentProps = {
searchRecord: SearchRecord;
objectMetadataItem: ObjectMetadataItem;
@@ -78,7 +72,7 @@ export const MultipleRecordPickerMenuItemContent = ({
multipleRecordPickerSearchableObjectMetadataItems.length > 1;
return (
<StyledSelectableItem
<SelectableListItem
itemId={searchRecord.recordId}
key={searchRecord.recordId}
onEnter={() => handleSelectChange(!isRecordSelectedWithObjectItem)}
@@ -103,6 +97,6 @@ export const MultipleRecordPickerMenuItemContent = ({
: undefined
}
/>
</StyledSelectableItem>
</SelectableListItem>
);
};
@@ -1,5 +1,3 @@
import { styled } from '@linaria/react';
import { getAvatarType } from '@/object-metadata/utils/getAvatarType';
import { searchRecordStoreFamilyState } from '@/object-record/record-picker/multiple-record-picker/states/searchRecordStoreComponentFamilyState';
import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext';
@@ -22,10 +20,6 @@ type SingleRecordPickerMenuItemProps = {
isRecordSelected: boolean;
};
const StyledSelectableItem = styled(SelectableListItem)`
width: 100%;
`;
export const SingleRecordPickerMenuItem = ({
morphItem,
onMorphItemSelected,
@@ -64,7 +58,7 @@ export const SingleRecordPickerMenuItem = ({
singleRecordPickerSearchableObjectMetadataItems.length > 1;
return (
<StyledSelectableItem
<SelectableListItem
itemId={morphItem.recordId}
key={morphItem.recordId}
onEnter={() => {
@@ -94,6 +88,6 @@ export const SingleRecordPickerMenuItem = ({
: undefined
}
/>
</StyledSelectableItem>
</SelectableListItem>
);
};
@@ -0,0 +1,21 @@
import { RECORD_TABLE_HORIZONTAL_SCROLL_SHADOW_VISIBILITY_CSS_VARIABLE_NAME } from '@/object-record/record-table/constants/RecordTableHorizontalScrollShadowVisibilityCssVariableName';
import { themeCssVariables } from 'twenty-ui/theme-constants';
export const HorizontalScrollBoxShadowCSS = `
&::after {
content: '';
position: absolute;
top: -1px;
height: calc(100% + 2px);
width: 4px;
right: -1px;
box-shadow:
2px 0px 4px 0px ${themeCssVariables.boxShadow.color},
0px 0px 4px 0px ${themeCssVariables.boxShadow.color};
clip-path: inset(0px -4px 0px 0px);
visibility: var(
${RECORD_TABLE_HORIZONTAL_SCROLL_SHADOW_VISIBILITY_CSS_VARIABLE_NAME},
hidden
);
}
`;
@@ -9,51 +9,15 @@ import { RECORD_TABLE_COLUMN_LAST_EMPTY_COLUMN_WIDTH_CLASS_NAME } from '@/object
import { RECORD_TABLE_COLUMN_LAST_EMPTY_COLUMN_WIDTH_VARIABLE_NAME } from '@/object-record/record-table/constants/RecordTableColumnLastEmptyColumnWidthVariableName';
import { RECORD_TABLE_COLUMN_WITH_GROUP_LAST_EMPTY_COLUMN_WIDTH_CLASS_NAME } from '@/object-record/record-table/constants/RecordTableColumnWithGroupLastEmptyColumnWidthClassName';
import { RECORD_TABLE_COLUMN_WITH_GROUP_LAST_EMPTY_COLUMN_WIDTH_VARIABLE_NAME } from '@/object-record/record-table/constants/RecordTableColumnWithGroupLastEmptyColumnWidthVariableName';
import { RECORD_TABLE_HORIZONTAL_SCROLL_SHADOW_VISIBILITY_CSS_VARIABLE_NAME } from '@/object-record/record-table/constants/RecordTableHorizontalScrollShadowVisibilityCssVariableName';
import { RECORD_TABLE_VERTICAL_SCROLL_SHADOW_VISIBILITY_CSS_VARIABLE_NAME } from '@/object-record/record-table/constants/RecordTableVerticalScrollShadowVisibilityCssVariableName';
import { TABLE_Z_INDEX } from '@/object-record/record-table/constants/TableZIndex';
import { HorizontalScrollBoxShadowCSS } from '@/object-record/record-table/components/HorizontalScrollBoxShadowCSS';
import { VerticalScrollBoxShadowCSS } from '@/object-record/record-table/components/VerticalScrollBoxShadowCSS';
import { getRecordTableColumnFieldWidthClassName } from '@/object-record/record-table/utils/getRecordTableColumnFieldWidthClassName';
import { getRecordTableColumnFieldWidthCSSVariableName } from '@/object-record/record-table/utils/getRecordTableColumnFieldWidthCSSVariableName';
import { styled } from '@linaria/react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
export const VerticalScrollBoxShadowCSS = `
&::before {
bottom: -1px;
box-shadow:
0px 2px 4px 0px ${themeCssVariables.boxShadow.color},
0px 0px 4px 0px ${themeCssVariables.boxShadow.color};
clip-path: inset(0px 0px -4px 0px);
content: '';
height: 4px;
position: absolute;
visibility: var(
${RECORD_TABLE_VERTICAL_SCROLL_SHADOW_VISIBILITY_CSS_VARIABLE_NAME},
hidden
);
width: 100%;
}
`;
export const HorizontalScrollBoxShadowCSS = `
&::after {
content: '';
position: absolute;
top: -1px;
height: calc(100% + 2px);
width: 4px;
right: -1px;
box-shadow:
2px 0px 4px 0px ${themeCssVariables.boxShadow.color},
0px 0px 4px 0px ${themeCssVariables.boxShadow.color};
clip-path: inset(0px -4px 0px 0px);
visibility: var(
${RECORD_TABLE_HORIZONTAL_SCROLL_SHADOW_VISIBILITY_CSS_VARIABLE_NAME},
hidden
);
}
`;
export { HorizontalScrollBoxShadowCSS, VerticalScrollBoxShadowCSS };
const MAX_COLUMNS = 100;
@@ -0,0 +1,20 @@
import { RECORD_TABLE_VERTICAL_SCROLL_SHADOW_VISIBILITY_CSS_VARIABLE_NAME } from '@/object-record/record-table/constants/RecordTableVerticalScrollShadowVisibilityCssVariableName';
import { themeCssVariables } from 'twenty-ui/theme-constants';
export const VerticalScrollBoxShadowCSS = `
&::before {
bottom: -1px;
box-shadow:
0px 2px 4px 0px ${themeCssVariables.boxShadow.color},
0px 0px 4px 0px ${themeCssVariables.boxShadow.color};
clip-path: inset(0px 0px -4px 0px);
content: '';
height: 4px;
position: absolute;
visibility: var(
${RECORD_TABLE_VERTICAL_SCROLL_SHADOW_VISIBILITY_CSS_VARIABLE_NAME},
hidden
);
width: 100%;
}
`;
@@ -1,7 +1,5 @@
import {
getRecordTableColumnWidthInlineStyles,
HorizontalScrollBoxShadowCSS,
} from '@/object-record/record-table/components/RecordTableStyleWrapper';
import { getRecordTableColumnWidthInlineStyles } from '@/object-record/record-table/components/RecordTableStyleWrapper';
import { HorizontalScrollBoxShadowCSS } from '@/object-record/record-table/components/HorizontalScrollBoxShadowCSS';
import { RECORD_TABLE_COLUMN_ADD_COLUMN_BUTTON_WIDTH } from '@/object-record/record-table/constants/RecordTableColumnAddColumnButtonWidth';
import { RECORD_TABLE_COLUMN_ADD_COLUMN_BUTTON_WIDTH_CLASS_NAME } from '@/object-record/record-table/constants/RecordTableColumnAddColumnButtonWidthClassName';
import { RECORD_TABLE_COLUMN_CHECKBOX_WIDTH } from '@/object-record/record-table/constants/RecordTableColumnCheckboxWidth';
@@ -34,10 +32,9 @@ import {
type DraggableRubric,
type DraggableStateSnapshot,
} from '@hello-pangea/dnd';
import { useContext, useMemo } from 'react';
import { useMemo } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { ThemeContext } from 'twenty-ui/theme';
import { MOBILE_VIEWPORT } from 'twenty-ui/theme-constants';
import { MOBILE_VIEWPORT, themeCssVariables } from 'twenty-ui/theme-constants';
const MAX_COLUMNS = 100;
@@ -139,8 +136,6 @@ export const RecordTableBodyVirtualizedDraggableClone = ({
}) => {
const realIndex = rubric.source.index;
const { theme } = useContext(ThemeContext);
const recordId = useAtomComponentFamilySelectorValue(
recordIdByRealIndexComponentFamilySelector,
realIndex,
@@ -178,10 +173,10 @@ export const RecordTableBodyVirtualizedDraggableClone = ({
style={{
...draggableProvided.draggableProps.style,
background: draggableSnapshot.isDragging
? theme.background.transparent.light
? themeCssVariables.background.transparent.light
: undefined,
borderColor: draggableSnapshot.isDragging
? `${theme.border.color.medium}`
? themeCssVariables.border.color.medium
: 'transparent',
opacity: isSecondaryDragged ? 0.3 : undefined,
}}