[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:
+2
-7
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+6
-7
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+4
-4
@@ -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;
|
||||
|
||||
+4
-5
@@ -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 {
|
||||
|
||||
+13
-7
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
-8
@@ -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;
|
||||
|
||||
+2
-8
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+2
-8
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+21
@@ -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
|
||||
);
|
||||
}
|
||||
`;
|
||||
+3
-39
@@ -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;
|
||||
|
||||
|
||||
+20
@@ -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%;
|
||||
}
|
||||
`;
|
||||
+6
-11
@@ -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,
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user