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:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user