Deprecate runtime theme objects in favor of CSS variables (#18402)

## Summary

- **Eliminate `ICON_SIZES` / `ICON_STROKES` constants**: all icon
dimensions are now resolved at runtime via
`resolveThemeVariableAsNumber(themeCssVariables.icon.size.X)`, ensuring
values always come from computed CSS variables
- **No more consumer imports from `twenty-ui/theme`**: moved
`ColorSchemeContext`, `ColorSchemeProvider`, `ThemeColor`,
`MAIN_COLOR_NAMES`, `getNextThemeColor`, `AnimationDuration` to
`twenty-ui/theme-constants`
- **Remove `ThemeContext` / `ThemeContextProvider` / `ThemeProvider` /
`ThemeType`**: replaced across ~300 files with `themeCssVariables` (for
CSS contexts) or `resolveThemeVariable` / `resolveThemeVariableAsNumber`
(for JS runtime values)
- **Simplify provider chain**: only `ColorSchemeProvider` remains — it
toggles `light`/`dark` class on `document.documentElement` and provides
`colorScheme` via React context
- **Fix pre-existing test failures**: `useIcons.test.ts`
(non-configurable ES module spy) and
`turnRecordFilterGroupIntoGqlOperationFilter.test.ts`
(`Omit<RecordFilter, 'id'>` type mismatch)

### Theme access pattern (before → after)

| Context | Before | After |
|---------|--------|-------|
| CSS (Linaria) | `${({ theme }) => theme.font.color.primary}` |
`${themeCssVariables.font.color.primary}` |
| JS runtime (icon size, animation) | `theme.icon.size.md` /
`ICON_SIZES.md` |
`resolveThemeVariableAsNumber(themeCssVariables.icon.size.md)` |
| Color scheme check | `theme.name === 'dark'` |
`useContext(ColorSchemeContext).colorScheme === 'dark'` |
This commit is contained in:
Charles Bochet
2026-03-05 14:39:01 +01:00
committed by GitHub
parent 7293d4c1f8
commit 647c32ff3e
432 changed files with 1243 additions and 1433 deletions
@@ -1,17 +1,15 @@
import { styled } from '@linaria/react';
import { useContext } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { ThemeContext, type ThemeType } from '@ui/theme';
import { themeCssVariables } from '@ui/theme-constants';
const StyledLayout = styled.div<{
width?: number;
backgroundColor?: string | undefined;
height: number | 'fit-content';
theme: ThemeType;
}>`
background: ${({ theme, backgroundColor }) =>
backgroundColor ?? theme.background.primary};
border: 1px solid ${({ theme }) => theme.border.color.light};
background: ${({ backgroundColor }) =>
backgroundColor ?? themeCssVariables.background.primary};
border: 1px solid ${themeCssVariables.border.color.light};
border-radius: 5px;
display: flex;
@@ -42,14 +40,11 @@ export const ComponentStorybookLayout = ({
height,
children,
}: ComponentStorybookLayoutProps) => {
const { theme } = useContext(ThemeContext);
return (
<StyledLayout
width={width}
backgroundColor={backgroundColor}
height={isDefined(height) ? height : 'fit-content'}
theme={theme}
>
{children}
</StyledLayout>
@@ -1,36 +1,36 @@
import { styled } from '@linaria/react';
import { isNumber, isString } from '@sniptt/guards';
import { type Decorator } from '@storybook/react-vite';
import { type ComponentProps, type JSX, useContext } from 'react';
import { ThemeContext, type ThemeType } from '@ui/theme';
import { type ComponentProps, type JSX } from 'react';
import { themeCssVariables } from '@ui/theme-constants';
const StyledColumnTitle = styled.h1<{ theme: ThemeType }>`
font-size: ${({ theme }) => theme.font.size.lg};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
margin: ${({ theme }) => theme.spacing(2)};
const StyledColumnTitle = styled.h1`
font-size: ${themeCssVariables.font.size.lg};
font-weight: ${themeCssVariables.font.weight.semiBold};
margin: ${themeCssVariables.spacing[2]};
`;
const StyledRowsTitle = styled.h2<{ theme: ThemeType }>`
color: ${({ theme }) => theme.font.color.secondary};
font-size: ${({ theme }) => theme.font.size.md};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
margin: ${({ theme }) => theme.spacing(2)};
const StyledRowsTitle = styled.h2`
color: ${themeCssVariables.font.color.secondary};
font-size: ${themeCssVariables.font.size.md};
font-weight: ${themeCssVariables.font.weight.semiBold};
margin: ${themeCssVariables.spacing[2]};
width: 100px;
`;
const StyledRowTitle = styled.h3<{ theme: ThemeType }>`
color: ${({ theme }) => theme.font.color.tertiary};
font-size: ${({ theme }) => theme.font.size.md};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
margin: ${({ theme }) => theme.spacing(2)};
const StyledRowTitle = styled.h3`
color: ${themeCssVariables.font.color.tertiary};
font-size: ${themeCssVariables.font.size.md};
font-weight: ${themeCssVariables.font.weight.semiBold};
margin: ${themeCssVariables.spacing[2]};
width: 100px;
`;
const StyledElementTitle = styled.span<{ theme: ThemeType }>`
color: ${({ theme }) => theme.font.color.light};
font-size: ${({ theme }) => theme.font.size.xs};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
margin-bottom: ${({ theme }) => theme.spacing(1)};
const StyledElementTitle = styled.span`
color: ${themeCssVariables.font.color.light};
font-size: ${themeCssVariables.font.size.xs};
font-weight: ${themeCssVariables.font.weight.semiBold};
margin-bottom: ${themeCssVariables.spacing[1]};
text-align: center;
text-transform: uppercase;
`;
@@ -40,23 +40,23 @@ const StyledContainer = styled.div`
flex-direction: row;
`;
const StyledColumnContainer = styled.div<{ theme: ThemeType }>`
const StyledColumnContainer = styled.div`
display: flex;
flex-direction: column;
padding: ${({ theme }) => theme.spacing(2)};
padding: ${themeCssVariables.spacing[2]};
`;
const StyledRowsContainer = styled.div<{ theme: ThemeType }>`
const StyledRowsContainer = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(2)};
gap: ${themeCssVariables.spacing[2]};
`;
const StyledRowContainer = styled.div<{ theme: ThemeType }>`
const StyledRowContainer = styled.div`
display: flex;
flex: 1;
flex-direction: row;
gap: ${({ theme }) => theme.spacing(2)};
gap: ${themeCssVariables.spacing[2]};
`;
const StyledElementContainer = styled.div<{ width: number }>`
@@ -64,11 +64,11 @@ const StyledElementContainer = styled.div<{ width: number }>`
min-width: ${({ width }) => (width ? `${width}px` : 'auto')};
`;
const StyledCellContainer = styled.div<{ theme: ThemeType }>`
const StyledCellContainer = styled.div`
align-items: center;
display: flex;
flex-direction: column;
padding: ${({ theme }) => theme.spacing(2)};
padding: ${themeCssVariables.spacing[2]};
`;
const emptyDimension = {
@@ -98,8 +98,6 @@ export type CatalogOptions = {
};
export const CatalogDecorator: Decorator = (Story, context) => {
const { theme } = useContext(ThemeContext);
const {
catalog: { dimensions = [], options = {} } = {
dimensions: [],
@@ -121,31 +119,27 @@ export const CatalogDecorator: Decorator = (Story, context) => {
return (
<StyledContainer>
{dimension4.values.map((value4: any, index4: number) => (
<StyledColumnContainer key={`d4-${index4}`} theme={theme}>
<StyledColumnTitle theme={theme}>
<StyledColumnContainer key={`d4-${index4}`}>
<StyledColumnTitle>
{dimension4.labels?.(value4) ??
(isStringOrNumber(value4) ? value4 : '')}
</StyledColumnTitle>
{dimension3.values.map((value3: any, index3: number) => (
<StyledRowsContainer key={`d3-${index3}`} theme={theme}>
<StyledRowsTitle theme={theme}>
<StyledRowsContainer key={`d3-${index3}`}>
<StyledRowsTitle>
{dimension3.labels?.(value3) ??
(isStringOrNumber(value3) ? value3 : '')}
</StyledRowsTitle>
{dimension2.values.map((value2: any, index2: number) => (
<StyledRowContainer key={`d2-${index2}`} theme={theme}>
<StyledRowTitle theme={theme}>
<StyledRowContainer key={`d2-${index2}`}>
<StyledRowTitle>
{dimension2.labels?.(value2) ??
(isStringOrNumber(value2) ? value2 : '')}
</StyledRowTitle>
{dimension1.values.map((value1: any, index1: number) => {
return (
<StyledCellContainer
key={`d1-${index1}`}
id={value1}
theme={theme}
>
<StyledElementTitle theme={theme}>
<StyledCellContainer key={`d1-${index1}`} id={value1}>
<StyledElementTitle>
{dimension1.labels?.(value1) ??
(isStringOrNumber(value1) ? value1 : '')}
</StyledElementTitle>
@@ -1,6 +1,6 @@
import { type Decorator } from '@storybook/react-vite';
import { GRAY_SCALE_LIGHT, MAIN_COLORS_LIGHT } from '@ui/theme';
import { themeCssVariables } from '@ui/theme-constants';
import { ComponentStorybookLayout } from '../ComponentStorybookLayout';
@@ -9,11 +9,11 @@ const getBackgroundColor = (inverted: boolean, accent: string) => {
switch (accent) {
case 'default':
return GRAY_SCALE_LIGHT.gray11;
return themeCssVariables.grayScale.gray11;
case 'danger':
return MAIN_COLORS_LIGHT.red;
return themeCssVariables.color.red;
case 'blue':
return MAIN_COLORS_LIGHT.blue;
return themeCssVariables.color.blue;
default:
return undefined;
}