Skip to content

chore(twenty-front): migrate small modules from Emotion to Linaria (PR 1/10)#18314

Merged
charlesBochet merged 12 commits intomainfrom
more-linaria
Mar 2, 2026
Merged

chore(twenty-front): migrate small modules from Emotion to Linaria (PR 1/10)#18314
charlesBochet merged 12 commits intomainfrom
more-linaria

Conversation

@charlesBochet
Copy link
Copy Markdown
Member

@charlesBochet charlesBochet commented Mar 1, 2026

Emotion → Linaria migration — PR 1 of 10

First batch of the twenty-front migration from Emotion (runtime CSS-in-JS) to Linaria (zero-runtime, build-time extraction via wyw-in-js). Covers 100 files across 10 small standalone modules — chosen as the lowest-risk starting point.

Modules migrated

spreadsheet-import (28) · navigation-menu-item (17) · views (14) · billing (10) · blocknote-editor (7) · advanced-text-editor (7) · favorites (7) · navigation (4) · information-banner (3) · sign-in-background-mock (3)

Migration pattern

Every file follows the same mechanical transformation:

Emotion Linaria
import styled from '@emotion/styled' import { styled } from '@linaria/react'
${({ theme }) => theme.font.color.primary} ${themeCssVariables.font.color.primary}
${({ theme }) => theme.spacing(4)} ${themeCssVariables.spacing[4]}
const theme = useTheme() const { theme } = useContext(ThemeContext)
import { type Theme } from '@emotion/react' import { type ThemeType } from 'twenty-ui/theme'

themeCssVariables is a build-time object where every leaf is a var(--t-xxx) CSS custom property reference, evaluated statically by wyw-in-js. Runtime theme access (icon sizes, colors passed as props) uses useContext(ThemeContext).

Gotchas encountered & fixed

  • Interpolation return types — wyw-in-js requires string | number, never false/undefined. Replaced condition && 'css' with condition ? 'css' : ''.
  • css tag inside styled templates — Linaria css returns a class name, not CSS text. Replaced with plain template strings.
  • styled(Component) needs className — added className prop to NavigationDrawerSection, DropdownMenuItemsContainer, and Heading.
  • shouldForwardProp not supported — Linaria filters invalid DOM props automatically for HTML elements. For custom components, used wrapper divs where needed.
  • FormFieldPlaceholderStyles — converted from Emotion css function to a static string using themeCssVariables.

… move divider to Chip, fix Storybook

- Fix wyw-in-js Storybook crash by setting enforce: 'pre' on the Vite plugin
- Rename AvatarChip to AvatarOrIcon to better reflect its purpose
- Move rightComponentDivider from AvatarOrIcon to Chip/LinkChip where it belongs
- Remove unused MultipleAvatarChip component (zero consumers)
- Migrate CalendarEventDetails and FileIcon to use AvatarOrIcon
- Enhance Chip and LinkChip stories with full CatalogDecorator coverage

Made-with: Cursor
Breaks ~998 files into 10 PRs of ~100 files each, ordered from
lowest-risk (small standalone modules) to highest-complexity
(settings, object-record). Includes migration patterns, risk
assessment, and validation checklist.

Made-with: Cursor
Copilot AI review requested due to automatic review settings March 1, 2026 16:25
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 1, 2026

Too many files changed for review. (124 files found, 100 file limit)

@charlesBochet charlesBochet changed the base branch from main to rework-avatar-chip March 1, 2026 16:28
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR is the first of a 10-part series migrating twenty-front from Emotion (runtime CSS-in-JS) to Linaria (zero-runtime, build-time extraction). It covers ~100 files across 10 small standalone modules, replaces @emotion/styled with @linaria/react's styled, converts theme prop-functions to themeCssVariables references, and replaces useTheme() with useContext(ThemeContext). It also renames AvatarChip to AvatarOrIcon, removes MultipleAvatarChip, and adds a rightComponentDivider prop to Chip/LinkChip.

Changes:

  • Migrate ~100 files from Emotion to Linaria across 10 modules (billing, views, spreadsheet-import, navigation-menu-item, blocknote-editor, advanced-text-editor, favorites, navigation, information-banner, sign-in-background-mock)
  • Rename AvatarChipAvatarOrIcon and delete MultipleAvatarChip; add rightComponentDivider prop to Chip and LinkChip
  • Add docs/emotion-to-linaria-migration-plan.md covering all 10 planned PRs and patterns

Reviewed changes

Copilot reviewed 104 out of 104 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/twenty-ui/vite.config.ts / vite.config.individual.ts Add enforce: 'pre' to wyw plugin to ensure correct processing order
packages/twenty-ui/src/components/index.ts Replace AvatarChip/MultipleAvatarChip exports with AvatarOrIcon
packages/twenty-ui/src/components/avatar-or-icon/AvatarOrIcon.tsx Rename component, remove divider prop, complete Linaria migration
packages/twenty-ui/src/components/chip/Chip.tsx / LinkChip.tsx Add rightComponentDivider prop with divider UI element
packages/twenty-ui/src/components/chip/__stories__/Chip.stories.tsx / LinkChip.stories.tsx Expand story coverage with new variants and catalog stories
packages/twenty-ui/src/components/avatar-chip/MultipleAvatarChip.tsx Deleted entirely
packages/twenty-front/src/modules/views/** Linaria migration for views module (14 files)
packages/twenty-front/src/modules/spreadsheet-import/** Linaria migration for spreadsheet-import module (28 files)
packages/twenty-front/src/modules/billing/** Linaria migration for billing module (10 files)
packages/twenty-front/src/modules/navigation-menu-item/** Linaria migration for navigation-menu-item module (17 files)
packages/twenty-front/src/modules/favorites/** Linaria migration for favorites module (7 files)
packages/twenty-front/src/modules/blocknote-editor/** Linaria migration for blocknote-editor module (7 files)
packages/twenty-front/src/modules/advanced-text-editor/** Linaria migration for advanced-text-editor module (7 files)
packages/twenty-front/src/modules/navigation/** Linaria migration for navigation module (4 files)
packages/twenty-front/src/modules/information-banner/** Linaria migration for information-banner module (3 files)
packages/twenty-front/src/modules/sign-in-background-mock/** Linaria migration for sign-in-background-mock module (3 files)
packages/twenty-front/src/modules/ai/components/internal/AgentChatFilePreview.tsx Partial migration: AvatarChip→AvatarOrIcon but useTheme not replaced
packages/twenty-front/src/modules/file/components/FileIcon.tsx Still uses Emotion, not migrated
packages/twenty-front/src/modules/activities/calendar/components/CalendarEventDetails.tsx AvatarOrIcon introduced but still uses useTheme
packages/twenty-front/src/modules/object-record/record-field/ui/form-types/constants/FormFieldPlaceholderStyles.ts Converted from Emotion css function to static string
docs/emotion-to-linaria-migration-plan.md New migration reference document

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

gap: ${({ isExpanded }) => (isExpanded ? themeCssVariables.spacing[2] : 0)};
width: 100%;
transition: gap ${({ theme }) => theme.animation.duration.normal}s ease;
transition: gap ${themeCssVariables.animation.duration.normal}s ease;
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The transition CSS property in StyledRow uses ${themeCssVariables.animation.duration.normal}s ease. However, themeCssVariables.animation.duration.normal is a CSS var() reference (e.g. var(--t-animation-duration-normal)). Appending s directly to a CSS custom property (var(--x)s) is invalid CSS. Per the migration plan (section 8), this should instead use calc(${themeCssVariables.animation.duration.normal} * 1s) ease.

Copilot uses AI. Check for mistakes.
Comment on lines +97 to +115
height ${themeCssVariables.animation.duration.normal}s ease,
padding ${themeCssVariables.animation.duration.normal}s ease;
`;

const StyledNewChatButton = styled.div`
align-items: center;
justify-content: center;
display: flex;
cursor: pointer;
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ theme }) => theme.font.weight.medium};
gap: ${({ theme }) => theme.spacing(1)};
font-size: ${themeCssVariables.font.size.sm};
font-weight: ${themeCssVariables.font.weight.medium};
gap: ${themeCssVariables.spacing[1]};
height: 100%;
width: 100%;
border-radius: inherit;
color: ${({ theme }) => theme.font.color.secondary};
color: ${themeCssVariables.font.color.secondary};
transition:
background ${({ theme }) => theme.animation.duration.fast}s ease,
color ${({ theme }) => theme.animation.duration.fast}s ease;
background ${themeCssVariables.animation.duration.fast}s ease,
color ${themeCssVariables.animation.duration.fast}s ease;
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The transition properties in StyledNewChatButtonWrapper and StyledNewChatButton also use ${themeCssVariables.animation.duration.normal}s ease and ${themeCssVariables.animation.duration.fast}s ease patterns. CSS custom properties (CSS var()) cannot be concatenated directly with a unit suffix like s. These will produce invalid CSS such as var(--t-animation-duration-normal)s ease. They should use calc(${themeCssVariables.animation.duration.normal} * 1s) instead, as documented in the migration plan section 8.

Copilot uses AI. Check for mistakes.
const IMAGE_MAX_WIDTH = 600;

const StyledNodeViewWrapper = styled(NodeViewWrapper)`
const StyledNodeViewWrapperContainer = styled.div<{
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In ResizableImageView.tsx, the original StyledNodeViewWrapper = styled(NodeViewWrapper) was replaced with StyledNodeViewWrapperContainer = styled.div (a plain div), and a separate <NodeViewWrapper> is now the outer element. This is a behavioral change: TipTap's NodeViewWrapper sets a data-node-view-wrapper attribute and manages its own rendering context. Wrapping it with an extra div may affect TipTap's node view layout, positioning, and selection behavior. The styled div should ideally still be styled(NodeViewWrapper) if that API supports it with Linaria, or the nesting structure should be validated.

Suggested change
const StyledNodeViewWrapperContainer = styled.div<{
const StyledNodeViewWrapperContainer = styled(NodeViewWrapper)<{

Copilot uses AI. Check for mistakes.
Comment on lines 48 to +52
const StyledBar = styled.div`
align-items: center;
align-items: center;
border-top: 1px solid ${({ theme }) => theme.border.color.light};
border-top: 1px solid ${({ theme }) => theme.border.color.light};
border-top: 1px solid ${themeCssVariables.border.color.light};
border-top: 1px solid ${themeCssVariables.border.color.light};
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The StyledBar styled component in ViewBarDetails.tsx has duplicated CSS declarations that were carried over from the original Emotion code. Both align-items: center; and border-top: 1px solid ${themeCssVariables.border.color.light}; appear twice. The duplicates should be removed.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indeed

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 124 files

Note: This PR contains a large number of files. cubic only reviews up to 75 files per PR, so some files may not have been reviewed.

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerTabsRow.tsx">

<violation number="1" location="packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerTabsRow.tsx:33">
P1: Duration CSS variables are concatenated with `s` directly; use `calc(var * 1s)` so transition values are valid.</violation>

<violation number="2" location="packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerTabsRow.tsx:94">
P2: Unsupported fractional spacing key (`25.75`) can produce an invalid width; replace it with a literal pixel value.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

…R 1/10)

Migrate 100 files across 10 small standalone modules from
@emotion/styled + useTheme to @linaria/react + themeCssVariables:

- spreadsheet-import (28 files)
- navigation-menu-item (17 files)
- views (14 files)
- billing (10 files)
- blocknote-editor (7 files)
- advanced-text-editor (7 files)
- favorites (7 files)
- navigation (4 files)
- information-banner (3 files)
- sign-in-background-mock (3 files)

Also adds className support to NavigationDrawerSection and
DropdownMenuItemsContainer for Linaria styled() compatibility,
and converts FormFieldPlaceholderStyles to use themeCssVariables.

Made-with: Cursor
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 1, 2026

🚀 Preview Environment Ready!

Your preview environment is available at: http://bore.pub:6543

This environment will automatically shut down after 5 hours.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 168 files (changes from recent commits).

Note: This PR contains a large number of files. cubic only reviews up to 75 files per PR, so some files may not have been reviewed.

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/twenty-front/src/modules/spreadsheet-import/steps/components/SpreadsheetImportStepperContainer.tsx">

<violation number="1" location="packages/twenty-front/src/modules/spreadsheet-import/steps/components/SpreadsheetImportStepperContainer.tsx:13">
P2: Combine duplicate imports from `twenty-ui/theme-constants` into one statement to satisfy enforced no-duplicate-import rules.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 12 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/twenty-ui/project.json">

<violation number="1" location="packages/twenty-ui/project.json:46">
P2: `generateThemeConstants` should declare a build dependency; otherwise the target can fail when `dist` artifacts are missing.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Base automatically changed from rework-avatar-chip to main March 2, 2026 14:48
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 40 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/twenty-ui/project.json">

<violation number="1" location="packages/twenty-ui/project.json:49">
P1: This cache input set is incomplete for `generateThemeConstants`: the script consumes `dist/theme.cjs` and `dist/theme-constants.cjs`, so excluding them can cause stale cached generated theme constants.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

"executor": "nx:run-commands",
"cache": true,
"inputs": [
"{projectRoot}/scripts/generateThemeConstants.ts"
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: This cache input set is incomplete for generateThemeConstants: the script consumes dist/theme.cjs and dist/theme-constants.cjs, so excluding them can cause stale cached generated theme constants.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/twenty-ui/project.json, line 49:

<comment>This cache input set is incomplete for `generateThemeConstants`: the script consumes `dist/theme.cjs` and `dist/theme-constants.cjs`, so excluding them can cause stale cached generated theme constants.</comment>

<file context>
@@ -46,9 +46,7 @@
-        "{projectRoot}/scripts/generateThemeConstants.ts",
-        "{projectRoot}/dist/theme.cjs",
-        "{projectRoot}/dist/theme-constants.cjs"
+        "{projectRoot}/scripts/generateThemeConstants.ts"
       ],
       "outputs": [
</file context>
Suggested change
"{projectRoot}/scripts/generateThemeConstants.ts"
"{projectRoot}/scripts/generateThemeConstants.ts",
"{projectRoot}/dist/theme.cjs",
"{projectRoot}/dist/theme-constants.cjs"
Fix with Cubic

Made-with: Cursor

# Conflicts:
#	packages/twenty-front/src/locales/generated/af-ZA.ts
#	packages/twenty-front/src/locales/generated/ar-SA.ts
#	packages/twenty-front/src/locales/generated/ca-ES.ts
#	packages/twenty-front/src/locales/generated/cs-CZ.ts
#	packages/twenty-front/src/locales/generated/da-DK.ts
#	packages/twenty-front/src/locales/generated/de-DE.ts
#	packages/twenty-front/src/locales/generated/el-GR.ts
#	packages/twenty-front/src/locales/generated/en.ts
#	packages/twenty-front/src/locales/generated/es-ES.ts
#	packages/twenty-front/src/locales/generated/fi-FI.ts
#	packages/twenty-front/src/locales/generated/fr-FR.ts
#	packages/twenty-front/src/locales/generated/he-IL.ts
#	packages/twenty-front/src/locales/generated/hu-HU.ts
#	packages/twenty-front/src/locales/generated/it-IT.ts
#	packages/twenty-front/src/locales/generated/ja-JP.ts
#	packages/twenty-front/src/locales/generated/ko-KR.ts
#	packages/twenty-front/src/locales/generated/nl-NL.ts
#	packages/twenty-front/src/locales/generated/no-NO.ts
#	packages/twenty-front/src/locales/generated/pl-PL.ts
#	packages/twenty-front/src/locales/generated/pseudo-en.ts
#	packages/twenty-front/src/locales/generated/pt-BR.ts
#	packages/twenty-front/src/locales/generated/pt-PT.ts
#	packages/twenty-front/src/locales/generated/ro-RO.ts
#	packages/twenty-front/src/locales/generated/ru-RU.ts
#	packages/twenty-front/src/locales/generated/sr-Cyrl.ts
#	packages/twenty-front/src/locales/generated/sv-SE.ts
#	packages/twenty-front/src/locales/generated/tr-TR.ts
#	packages/twenty-front/src/locales/generated/uk-UA.ts
#	packages/twenty-front/src/locales/generated/vi-VN.ts
#	packages/twenty-front/src/locales/generated/zh-CN.ts
#	packages/twenty-front/src/locales/generated/zh-TW.ts
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 7 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/twenty-front/src/modules/file/hooks/useFileIconColors.ts">

<violation number="1" location="packages/twenty-front/src/modules/file/hooks/useFileIconColors.ts:14">
P2: `IMAGE` color mapping is inconsistent with the existing file-category color map, which can cause mismatched icon colors across the UI.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

return {
ARCHIVE: theme.color.gray,
AUDIO: theme.color.pink,
IMAGE: theme.color.amber,
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: IMAGE color mapping is inconsistent with the existing file-category color map, which can cause mismatched icon colors across the UI.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/twenty-front/src/modules/file/hooks/useFileIconColors.ts, line 14:

<comment>`IMAGE` color mapping is inconsistent with the existing file-category color map, which can cause mismatched icon colors across the UI.</comment>

<file context>
@@ -0,0 +1,21 @@
+  return {
+    ARCHIVE: theme.color.gray,
+    AUDIO: theme.color.pink,
+    IMAGE: theme.color.amber,
+    PRESENTATION: theme.color.orange,
+    SPREADSHEET: theme.color.turquoise,
</file context>
Fix with Cubic

@charlesBochet charlesBochet merged commit c4140f8 into main Mar 2, 2026
69 checks passed
@charlesBochet charlesBochet deleted the more-linaria branch March 2, 2026 15:33
@twenty-eng-sync
Copy link
Copy Markdown

Hey @charlesBochet! After you've done the QA of your Pull Request, you can mark it as done here. Thank you!

groovydr pushed a commit to fuse-gtm/fuse-v1 that referenced this pull request Mar 5, 2026
…R 1/10) (twentyhq#18314)

## Emotion → Linaria migration — PR 1 of 10

First batch of the `twenty-front` migration from Emotion (runtime
CSS-in-JS) to Linaria (zero-runtime, build-time extraction via
wyw-in-js). Covers **100 files** across 10 small standalone modules —
chosen as the lowest-risk starting point.

### Modules migrated

spreadsheet-import (28) · navigation-menu-item (17) · views (14) ·
billing (10) · blocknote-editor (7) · advanced-text-editor (7) ·
favorites (7) · navigation (4) · information-banner (3) ·
sign-in-background-mock (3)

### Migration pattern

Every file follows the same mechanical transformation:

| Emotion | Linaria |
|---|---|
| `import styled from '@emotion/styled'` | `import { styled } from
'@linaria/react'` |
| `${({ theme }) => theme.font.color.primary}` |
`${themeCssVariables.font.color.primary}` |
| `${({ theme }) => theme.spacing(4)}` |
`${themeCssVariables.spacing[4]}` |
| `const theme = useTheme()` | `const { theme } =
useContext(ThemeContext)` |
| `import { type Theme } from '@emotion/react'` | `import { type
ThemeType } from 'twenty-ui/theme'` |

`themeCssVariables` is a build-time object where every leaf is a
`var(--t-xxx)` CSS custom property reference, evaluated statically by
wyw-in-js. Runtime theme access (icon sizes, colors passed as props)
uses `useContext(ThemeContext)`.

### Gotchas encountered & fixed

- **Interpolation return types** — wyw-in-js requires `string | number`,
never `false`/`undefined`. Replaced `condition && 'css'` with `condition
? 'css' : ''`.
- **`css` tag inside `styled` templates** — Linaria `css` returns a
class name, not CSS text. Replaced with plain template strings.
- **`styled(Component)` needs `className`** — added `className` prop to
`NavigationDrawerSection`, `DropdownMenuItemsContainer`, and `Heading`.
- **`shouldForwardProp` not supported** — Linaria filters invalid DOM
props automatically for HTML elements. For custom components, used
wrapper divs where needed.
- **`FormFieldPlaceholderStyles`** — converted from Emotion `css`
function to a static string using `themeCssVariables`.

(cherry picked from commit c4140f8)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants