chore(twenty-front): migrate command-menu, workflow, page-layout and UI modules from Emotion to Linaria (PR 4-6/10) (#18342)

## Summary

Continues the Emotion → Linaria migration (PR 4-6 from the [migration
plan](docs/emotion-to-linaria-migration-plan.md)). Migrates **311
files** across four module groups:

| Module | Files |
|---|---|
| command-menu | 53 |
| workflow | 84 |
| page-layout | 84 |
| UI (partial - first ~80 files) | ~80 |
| twenty-ui (TEXT_INPUT_STYLE) | 1 |
| misc (hooks, keyboard-shortcut-menu, file-upload) | ~9 |

### Migration patterns applied

- `import styled from '@emotion/styled'` → `import { styled } from
'@linaria/react'`
- `import { useTheme } from '@emotion/react'` → `import { useContext }
from 'react'` + `import { ThemeContext } from 'twenty-ui/theme'`
- `${({ theme }) => theme.X.Y.Z}` → `${themeCssVariables.X.Y.Z}` (static
CSS variables)
- `theme.spacing(N)` → `themeCssVariables.spacing[N]`
- `styled(motion.div)` → `motion.create(StyledBase)` (11 components)
- `styled(Component)<TypeParams>` → wrapper div approach for non-HTML
elements
- Multi-declaration interpolations split into one CSS property per
interpolation
- Interpolation return types fixed (`&&` → ternary `? : ''`)
- `TEXT_INPUT_STYLE` converted from function to static string constant
(backward compatible)
- Emotion `<Global>` replaced with `useEffect` style injection
- Complex runtime-dependent styles use CSS custom properties via
`style={}` prop

### After this PR

- **Remaining files**: ~400 (object-record: ~160, settings: ~200, UI:
~44)
- **No breaking changes**: CSS variables resolve identically to the
previous Emotion theme values
This commit is contained in:
Charles Bochet
2026-03-03 16:42:03 +01:00
committed by GitHub
parent 8b26020a0b
commit 3bfdc2c83f
339 changed files with 3439 additions and 2613 deletions
@@ -4,7 +4,7 @@ import { differenceInSeconds, endOfDay, format } from 'date-fns';
import { CalendarEventRow } from '@/activities/calendar/components/CalendarEventRow';
import { getCalendarEventStartDate } from '@/activities/calendar/utils/getCalendarEventStartDate';
import { CardContent } from 'twenty-ui/layout';
import { motion } from 'framer-motion';
import { ThemeContext } from 'twenty-ui/theme';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { type TimelineCalendarEvent } from '~/generated/graphql';
@@ -14,15 +14,19 @@ type CalendarDayCardContentProps = {
divider?: boolean;
};
const StyledCardContent = styled(CardContent)`
const StyledCardContentBase = styled.div<{ divider?: boolean }>`
align-items: flex-start;
border-color: ${themeCssVariables.border.color.light};
background-color: ${themeCssVariables.background.secondary};
border-bottom: ${({ divider }) =>
divider ? `1px solid ${themeCssVariables.border.color.light}` : 'none'};
display: flex;
flex-direction: row;
gap: ${themeCssVariables.spacing[3]};
padding: ${themeCssVariables.spacing[2]} ${themeCssVariables.spacing[3]};
`;
const StyledCardContent = motion.create(StyledCardContentBase);
const StyledDayContainer = styled.div`
text-align: center;
width: ${themeCssVariables.spacing[6]};
@@ -19,7 +19,6 @@ import {
} from '@/object-record/record-field/ui/contexts/FieldContext';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
import { useIsRecordReadOnly } from '@/object-record/read-only/hooks/useIsRecordReadOnly';
import { isRecordFieldReadOnly } from '@/object-record/read-only/utils/isRecordFieldReadOnly';
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
@@ -53,10 +52,14 @@ const StyledContainer = styled.div`
box-sizing: border-box;
`;
const StyledEventChip = styled(Chip)`
gap: ${themeCssVariables.spacing[2]};
padding-left: ${themeCssVariables.spacing[2]};
padding-right: ${themeCssVariables.spacing[2]};
const StyledEventChipWrapper = styled.span`
display: inline-flex;
& > [data-testid='chip'] {
gap: ${themeCssVariables.spacing[2]};
padding-left: ${themeCssVariables.spacing[2]};
padding-right: ${themeCssVariables.spacing[2]};
}
`;
const StyledHeader = styled.header``;
@@ -67,7 +70,7 @@ const StyledTitle = styled.h2<{ canceled?: boolean }>`
margin: ${themeCssVariables.spacing[0]} ${themeCssVariables.spacing[0]}
${themeCssVariables.spacing[2]};
${({ canceled }) => (canceled ? 'text-decoration: line-through' : '')}
text-decoration: ${({ canceled }) => (canceled ? 'line-through' : 'none')};
`;
const StyledCreatedAt = styled.div`
@@ -81,9 +84,11 @@ const StyledFields = styled.div`
width: 100%;
`;
const StyledPropertyBox = styled(PropertyBox)`
const StyledPropertyBox = styled.div`
align-self: stretch;
display: flex;
flex-direction: column;
height: ${themeCssVariables.spacing[6]};
padding: 0;
width: 100%;
`;
@@ -199,14 +204,16 @@ export const CalendarEventDetails = ({
value={{ scopeInstanceId: INPUT_ID_PREFIX }}
>
<StyledContainer>
<StyledEventChip
accent={ChipAccent.TextSecondary}
size={ChipSize.Large}
variant={ChipVariant.Highlighted}
clickable={false}
leftComponent={<AvatarOrIcon Icon={IconCalendarEvent} />}
label={t`Event`}
/>
<StyledEventChipWrapper>
<Chip
accent={ChipAccent.TextSecondary}
size={ChipSize.Large}
variant={ChipVariant.Highlighted}
clickable={false}
leftComponent={<AvatarOrIcon Icon={IconCalendarEvent} />}
label={t`Event`}
/>
</StyledEventChipWrapper>
<StyledHeader>
<StyledTitle canceled={calendarEvent.isCanceled}>
{calendarEvent.title}
@@ -2,27 +2,29 @@ import { useContext } from 'react';
import { styled } from '@linaria/react';
import { Trans } from '@lingui/react/macro';
import { IconLock } from 'twenty-ui/display';
import { Card, CardContent } from 'twenty-ui/layout';
import { ThemeContext } from 'twenty-ui/theme';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledVisibilityCard = styled(Card)`
border-color: ${themeCssVariables.border.color.light};
const StyledVisibilityCard = styled.div`
border: 1px solid ${themeCssVariables.border.color.light};
border-radius: ${themeCssVariables.border.radius.sm};
color: ${themeCssVariables.font.color.light};
flex: 1;
overflow: hidden;
transition: color ${themeCssVariables.animation.duration.normal} ease;
width: 100%;
`;
const StyledVisibilityCardContent = styled(CardContent)`
const StyledVisibilityCardContent = styled.div`
align-items: center;
background-color: ${themeCssVariables.background.transparent.lighter};
box-sizing: border-box;
display: flex;
font-size: ${themeCssVariables.font.size.sm};
font-weight: ${themeCssVariables.font.weight.medium};
display: flex;
gap: ${themeCssVariables.spacing[1]};
padding: ${themeCssVariables.spacing[0]} ${themeCssVariables.spacing[1]};
height: ${themeCssVariables.spacing[6]};
background-color: ${themeCssVariables.background.transparent.lighter};
padding: ${themeCssVariables.spacing[0]} ${themeCssVariables.spacing[1]};
`;
export const CalendarEventNotSharedContent = () => {
@@ -3,7 +3,6 @@ import { styled } from '@linaria/react';
import { type CalendarEventParticipant } from '@/activities/calendar/types/CalendarEventParticipant';
import { ParticipantChip } from '@/activities/components/ParticipantChip';
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
import { EllipsisDisplay } from '@/ui/field/display/components/EllipsisDisplay';
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
import { IconCheck, IconQuestionMark, IconX } from 'twenty-ui/display';
@@ -22,9 +21,11 @@ const StyledInlineCellBaseContainer = styled.div`
user-select: none;
`;
const StyledPropertyBox = styled(PropertyBox)`
const StyledPropertyBox = styled.div`
align-self: stretch;
display: flex;
flex-direction: column;
height: ${themeCssVariables.spacing[6]};
padding: 0;
width: 100%;
`;
@@ -34,13 +34,13 @@ const StyledContainer = styled.div<{ showTitle?: boolean }>`
`;
const StyledAttendanceIndicator = styled.div<{ active?: boolean }>`
background-color: ${themeCssVariables.tag.background.gray};
background-color: ${({ active }) =>
active
? themeCssVariables.tag.background.red
: themeCssVariables.tag.background.gray};
height: 100%;
width: ${themeCssVariables.spacing[1]};
border-radius: ${themeCssVariables.border.radius.xs};
${({ active }) =>
active ? `background-color: ${themeCssVariables.tag.background.red}` : ''}
`;
const StyledLabels = styled.div`
@@ -60,17 +60,16 @@ const StyledTime = styled.div`
`;
const StyledTitle = styled.div<{ active: boolean; canceled: boolean }>`
color: ${({ active }) =>
active ? themeCssVariables.font.color.primary : 'inherit'};
flex: 1 0 auto;
font-weight: ${({ active }) =>
active ? themeCssVariables.font.weight.medium : 'inherit'};
overflow: hidden;
text-decoration: ${({ canceled }) => (canceled ? 'line-through' : 'none')};
text-overflow: ellipsis;
white-space: nowrap;
width: ${themeCssVariables.spacing[10]};
${({ active }) =>
active
? `color: ${themeCssVariables.font.color.primary}; font-weight: ${themeCssVariables.font.weight.medium}`
: ''}
${({ canceled }) => (canceled ? 'text-decoration: line-through' : '')}
`;
export const CalendarEventRow = ({
@@ -1,10 +1,11 @@
import { styled } from '@linaria/react';
import React from 'react';
import { CardContent } from 'twenty-ui/layout';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledRowContent = styled(CardContent)`
const StyledRowContent = styled.div<{ isClickable: boolean }>`
align-items: center;
background-color: ${themeCssVariables.background.secondary};
cursor: ${({ isClickable }) => (isClickable ? 'pointer' : 'default')};
display: flex;
gap: ${themeCssVariables.spacing[2]};
height: ${themeCssVariables.spacing[12]};
@@ -25,7 +25,7 @@ const StyledParticipantsContainer = styled.div`
display: flex;
`;
const StyledAvatar = styled(Avatar)`
const StyledAvatarWrapper = styled.div`
margin-left: calc(-1 * ${themeCssVariables.spacing[1]});
`;
@@ -123,24 +123,30 @@ export const EmailThreadPreview = ({ thread }: EmailThreadPreviewProps) => {
type="rounded"
/>
{thread?.lastTwoParticipants?.[0] && (
<StyledAvatar
avatarUrl={thread.lastTwoParticipants[0].avatarUrl}
placeholder={thread.lastTwoParticipants[0].displayName}
placeholderColorSeed={
thread.lastTwoParticipants[0].workspaceMemberId ||
thread.lastTwoParticipants[0].personId
}
type="rounded"
/>
<StyledAvatarWrapper>
<Avatar
avatarUrl={thread.lastTwoParticipants[0].avatarUrl}
placeholder={thread.lastTwoParticipants[0].displayName}
placeholderColorSeed={
thread.lastTwoParticipants[0].workspaceMemberId ||
thread.lastTwoParticipants[0].personId
}
type="rounded"
/>
</StyledAvatarWrapper>
)}
{finalDisplayedName && (
<StyledAvatar
avatarUrl={finalAvatarUrl}
placeholder={finalDisplayedName}
type="rounded"
color={isCountIcon ? theme.grayScale.gray11 : undefined}
backgroundColor={isCountIcon ? theme.grayScale.gray2 : undefined}
/>
<StyledAvatarWrapper>
<Avatar
avatarUrl={finalAvatarUrl}
placeholder={finalDisplayedName}
type="rounded"
color={isCountIcon ? theme.grayScale.gray11 : undefined}
backgroundColor={
isCountIcon ? theme.grayScale.gray2 : undefined
}
/>
</StyledAvatarWrapper>
)}
</StyledParticipantsContainer>