[DevXP] Simplify twenty-ui theme system: replace auto-generated files with static CSS variables (#18389)
## Summary Now that Twenty has fully migrated from Emotion to Linaria, the theme system has been simplified to remove unnecessary complexity that existed only to support the old runtime injection pattern. ### What changed - **Deleted** `generateThemeConstants.ts` script and the entire `generated/` directory — no more auto-generation - **Added** `theme-light.css` and `theme-dark.css`: static CSS files with 991 custom properties each, scoped under `.light` and `.dark` selectors respectively - **Moved** `themeCssVariables.ts` out of `generated/` and hand-maintain it as a static `as const` object of `var(--t-*)` references (Linaria can statically evaluate these at build time) - **Extracted** numeric constants (`MOBILE_VIEWPORT`, `ICON_SIZES`, `ICON_STROKES`) into a new `constants.ts` — CSS variables can't be used in media queries or as numeric icon size props - **Simplified** `ThemeContextProvider`: removed `ThemeCssVariableInjectorEffect` entirely; now uses a single `useLayoutEffect` to toggle `.light`/`.dark` class on `<html>` - **Added** `class="light"` to `index.html` as default to prevent FOUC before React hydration ### Why The previous setup maintained a dual system: JS theme objects (`THEME_LIGHT`/`THEME_DARK`) used at runtime, plus a generation script that produced CSS variable entry arrays, which were then injected into the DOM by `ThemeCssVariableInjectorEffect`. With Linaria, theme values only need to be CSS custom properties — the JS objects were redundant. This PR removes ~250 lines of infrastructure while keeping the same theming capabilities.
This commit is contained in:
+3
-7
@@ -1,10 +1,6 @@
|
||||
import { type WorkflowActionType } from '@/workflow/types/Workflow';
|
||||
import { getActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIconColorOrThrow';
|
||||
import { useContext } from 'react';
|
||||
import { ThemeContext } from 'twenty-ui/theme';
|
||||
|
||||
export const useActionIconColorOrThrow = (actionType: WorkflowActionType) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
|
||||
return getActionIconColorOrThrow({ theme, actionType });
|
||||
};
|
||||
export const useActionIconColorOrThrow = (
|
||||
actionType: WorkflowActionType,
|
||||
): string => getActionIconColorOrThrow(actionType);
|
||||
|
||||
+41
-282
@@ -1,309 +1,68 @@
|
||||
import { type WorkflowActionType } from '@/workflow/types/Workflow';
|
||||
import { COLOR_LIGHT, GRAY_SCALE_LIGHT, type ThemeType } from 'twenty-ui/theme';
|
||||
import { getActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIconColorOrThrow';
|
||||
|
||||
const mockTheme: ThemeType = {
|
||||
color: {
|
||||
orange: COLOR_LIGHT.orange,
|
||||
pink: COLOR_LIGHT.pink,
|
||||
red: COLOR_LIGHT.red,
|
||||
},
|
||||
font: {
|
||||
color: {
|
||||
tertiary: GRAY_SCALE_LIGHT.gray9,
|
||||
},
|
||||
},
|
||||
} as ThemeType;
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
describe('getActionIconColorOrThrow', () => {
|
||||
describe('action types that return red color', () => {
|
||||
const coreActionTypes: WorkflowActionType[] = [
|
||||
it('returns red for CODE, HTTP_REQUEST, SEND_EMAIL, DRAFT_EMAIL, LOGIC_FUNCTION', () => {
|
||||
const redActions: WorkflowActionType[] = [
|
||||
'CODE',
|
||||
'HTTP_REQUEST',
|
||||
'SEND_EMAIL',
|
||||
'DRAFT_EMAIL',
|
||||
'LOGIC_FUNCTION',
|
||||
];
|
||||
|
||||
coreActionTypes.forEach((actionType) => {
|
||||
it(`should return red color for ${actionType} action type`, () => {
|
||||
const result = getActionIconColorOrThrow({
|
||||
theme: mockTheme,
|
||||
actionType,
|
||||
});
|
||||
|
||||
expect(result).toBe(mockTheme.color.red);
|
||||
});
|
||||
redActions.forEach((actionType) => {
|
||||
expect(getActionIconColorOrThrow(actionType)).toBe(
|
||||
themeCssVariables.color.red,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('action types that return tertiary font color', () => {
|
||||
const recordActionTypes: WorkflowActionType[] = [
|
||||
it('returns tertiary font color for record actions', () => {
|
||||
const recordActions: WorkflowActionType[] = [
|
||||
'CREATE_RECORD',
|
||||
'UPDATE_RECORD',
|
||||
'DELETE_RECORD',
|
||||
'UPSERT_RECORD',
|
||||
'FIND_RECORDS',
|
||||
];
|
||||
|
||||
recordActionTypes.forEach((actionType) => {
|
||||
it(`should return tertiary font color for ${actionType} action type`, () => {
|
||||
const result = getActionIconColorOrThrow({
|
||||
theme: mockTheme,
|
||||
actionType,
|
||||
});
|
||||
|
||||
expect(result).toBe(mockTheme.font.color.tertiary);
|
||||
});
|
||||
recordActions.forEach((actionType) => {
|
||||
expect(getActionIconColorOrThrow(actionType)).toBe(
|
||||
themeCssVariables.font.color.tertiary,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('action types that return orange color', () => {
|
||||
it('should return orange color for FORM action type', () => {
|
||||
const result = getActionIconColorOrThrow({
|
||||
theme: mockTheme,
|
||||
actionType: 'FORM',
|
||||
});
|
||||
it('returns orange for FORM', () => {
|
||||
expect(getActionIconColorOrThrow('FORM')).toBe(
|
||||
themeCssVariables.color.orange,
|
||||
);
|
||||
});
|
||||
|
||||
expect(result).toBe(mockTheme.color.orange);
|
||||
it('returns green12 for ITERATOR, EMPTY, FILTER, IF_ELSE, DELAY', () => {
|
||||
const greenActions: WorkflowActionType[] = [
|
||||
'ITERATOR',
|
||||
'EMPTY',
|
||||
'FILTER',
|
||||
'IF_ELSE',
|
||||
'DELAY',
|
||||
];
|
||||
greenActions.forEach((actionType) => {
|
||||
expect(getActionIconColorOrThrow(actionType)).toBe(
|
||||
themeCssVariables.color.green12,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('action types that return pink color', () => {
|
||||
it('should return pink color for AI_AGENT action type', () => {
|
||||
const result = getActionIconColorOrThrow({
|
||||
theme: mockTheme,
|
||||
actionType: 'AI_AGENT',
|
||||
});
|
||||
|
||||
expect(result).toBe(mockTheme.color.pink);
|
||||
});
|
||||
it('returns pink for AI_AGENT', () => {
|
||||
expect(getActionIconColorOrThrow('AI_AGENT')).toBe(
|
||||
themeCssVariables.color.pink,
|
||||
);
|
||||
});
|
||||
|
||||
describe('FILTER action type', () => {
|
||||
it('should return green color for FILTER action type', () => {
|
||||
const result = getActionIconColorOrThrow({
|
||||
theme: mockTheme,
|
||||
actionType: 'FILTER',
|
||||
});
|
||||
|
||||
expect(result).toBe(mockTheme.color.green12);
|
||||
});
|
||||
});
|
||||
|
||||
describe('theme object handling', () => {
|
||||
it('should use the provided theme colors correctly', () => {
|
||||
const customTheme: ThemeType = {
|
||||
color: {
|
||||
red: COLOR_LIGHT.red,
|
||||
orange: COLOR_LIGHT.orange,
|
||||
pink: COLOR_LIGHT.turquoise,
|
||||
},
|
||||
font: {
|
||||
color: {
|
||||
tertiary: GRAY_SCALE_LIGHT.gray11,
|
||||
},
|
||||
},
|
||||
} as ThemeType;
|
||||
|
||||
expect(
|
||||
getActionIconColorOrThrow({
|
||||
theme: customTheme,
|
||||
actionType: 'CODE',
|
||||
}),
|
||||
).toBe(COLOR_LIGHT.red);
|
||||
|
||||
expect(
|
||||
getActionIconColorOrThrow({
|
||||
theme: customTheme,
|
||||
actionType: 'SEND_EMAIL',
|
||||
}),
|
||||
).toBe(COLOR_LIGHT.red);
|
||||
|
||||
expect(
|
||||
getActionIconColorOrThrow({
|
||||
theme: customTheme,
|
||||
actionType: 'AI_AGENT',
|
||||
}),
|
||||
).toBe(COLOR_LIGHT.turquoise);
|
||||
|
||||
expect(
|
||||
getActionIconColorOrThrow({
|
||||
theme: customTheme,
|
||||
actionType: 'CREATE_RECORD',
|
||||
}),
|
||||
).toBe(GRAY_SCALE_LIGHT.gray11);
|
||||
});
|
||||
});
|
||||
|
||||
describe('type safety and exhaustive checking', () => {
|
||||
it('should handle all valid action types without throwing unreachable errors', () => {
|
||||
const validActionTypes: WorkflowActionType[] = [
|
||||
'CODE',
|
||||
'HTTP_REQUEST',
|
||||
'CREATE_RECORD',
|
||||
'UPDATE_RECORD',
|
||||
'DELETE_RECORD',
|
||||
'FIND_RECORDS',
|
||||
'FORM',
|
||||
'SEND_EMAIL',
|
||||
'AI_AGENT',
|
||||
];
|
||||
|
||||
validActionTypes.forEach((actionType) => {
|
||||
expect(() => {
|
||||
getActionIconColorOrThrow({
|
||||
theme: mockTheme,
|
||||
actionType,
|
||||
});
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return consistent color values for the same action type', () => {
|
||||
const actionType: WorkflowActionType = 'CODE';
|
||||
const result1 = getActionIconColorOrThrow({
|
||||
theme: mockTheme,
|
||||
actionType,
|
||||
});
|
||||
const result2 = getActionIconColorOrThrow({
|
||||
theme: mockTheme,
|
||||
actionType,
|
||||
});
|
||||
|
||||
expect(result1).toBe(result2);
|
||||
expect(result1).toBe(mockTheme.color.red);
|
||||
});
|
||||
});
|
||||
|
||||
describe('color grouping logic', () => {
|
||||
it('should group CODE and HTTP_REQUEST actions with red color', () => {
|
||||
const orangeActions: WorkflowActionType[] = ['CODE', 'HTTP_REQUEST'];
|
||||
|
||||
orangeActions.forEach((actionType) => {
|
||||
const result = getActionIconColorOrThrow({
|
||||
theme: mockTheme,
|
||||
actionType,
|
||||
});
|
||||
expect(result).toBe(mockTheme.color.red);
|
||||
});
|
||||
});
|
||||
|
||||
it('should group record-related actions with tertiary font color', () => {
|
||||
const recordActions: WorkflowActionType[] = [
|
||||
'CREATE_RECORD',
|
||||
'UPDATE_RECORD',
|
||||
'DELETE_RECORD',
|
||||
'FIND_RECORDS',
|
||||
];
|
||||
|
||||
recordActions.forEach((actionType) => {
|
||||
const result = getActionIconColorOrThrow({
|
||||
theme: mockTheme,
|
||||
actionType,
|
||||
});
|
||||
expect(result).toBe(mockTheme.font.color.tertiary);
|
||||
});
|
||||
});
|
||||
|
||||
it('should have unique colors for different action categories', () => {
|
||||
const tertiaryResult = getActionIconColorOrThrow({
|
||||
theme: mockTheme,
|
||||
actionType: 'CREATE_RECORD',
|
||||
});
|
||||
|
||||
expect(tertiaryResult).toBe(mockTheme.font.color.tertiary);
|
||||
});
|
||||
|
||||
it('should return red color for SEND_EMAIL action type', () => {
|
||||
expect(
|
||||
getActionIconColorOrThrow({
|
||||
theme: mockTheme,
|
||||
actionType: 'SEND_EMAIL',
|
||||
}),
|
||||
).toBe(mockTheme.color.red);
|
||||
});
|
||||
|
||||
it('should return pink color for AI_AGENT action type', () => {
|
||||
expect(
|
||||
getActionIconColorOrThrow({ theme: mockTheme, actionType: 'AI_AGENT' }),
|
||||
).toBe(mockTheme.color.pink);
|
||||
});
|
||||
|
||||
it('should use the provided theme colors correctly', () => {
|
||||
const customTheme: ThemeType = {
|
||||
color: {
|
||||
red: COLOR_LIGHT.red,
|
||||
orange: COLOR_LIGHT.orange,
|
||||
pink: COLOR_LIGHT.turquoise,
|
||||
},
|
||||
font: {
|
||||
color: {
|
||||
tertiary: GRAY_SCALE_LIGHT.gray11,
|
||||
},
|
||||
},
|
||||
} as ThemeType;
|
||||
|
||||
expect(
|
||||
getActionIconColorOrThrow({ theme: customTheme, actionType: 'CODE' }),
|
||||
).toBe(COLOR_LIGHT.red);
|
||||
expect(
|
||||
getActionIconColorOrThrow({
|
||||
theme: customTheme,
|
||||
actionType: 'SEND_EMAIL',
|
||||
}),
|
||||
).toBe(COLOR_LIGHT.red);
|
||||
expect(
|
||||
getActionIconColorOrThrow({
|
||||
theme: customTheme,
|
||||
actionType: 'AI_AGENT',
|
||||
}),
|
||||
).toBe(COLOR_LIGHT.turquoise);
|
||||
expect(
|
||||
getActionIconColorOrThrow({
|
||||
theme: customTheme,
|
||||
actionType: 'CREATE_RECORD',
|
||||
}),
|
||||
).toBe(GRAY_SCALE_LIGHT.gray11);
|
||||
});
|
||||
|
||||
it('should return undefined when red color is missing for SEND_EMAIL action', () => {
|
||||
const themeWithoutBlue: ThemeType = {
|
||||
color: {
|
||||
orange: COLOR_LIGHT.orange,
|
||||
pink: COLOR_LIGHT.pink,
|
||||
},
|
||||
font: {
|
||||
color: {
|
||||
tertiary: GRAY_SCALE_LIGHT.gray9,
|
||||
},
|
||||
},
|
||||
} as ThemeType;
|
||||
|
||||
expect(
|
||||
getActionIconColorOrThrow({
|
||||
theme: themeWithoutBlue,
|
||||
actionType: 'SEND_EMAIL',
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle null theme gracefully', () => {
|
||||
expect(() => {
|
||||
getActionIconColorOrThrow({
|
||||
theme: null as unknown as ThemeType,
|
||||
actionType: 'CODE',
|
||||
});
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('should return the same color for the same action type', () => {
|
||||
const result1 = getActionIconColorOrThrow({
|
||||
theme: mockTheme,
|
||||
actionType: 'CODE',
|
||||
});
|
||||
const result2 = getActionIconColorOrThrow({
|
||||
theme: mockTheme,
|
||||
actionType: 'CODE',
|
||||
});
|
||||
expect(result1).toBe(result2);
|
||||
});
|
||||
it('returns consistent values for repeated calls', () => {
|
||||
expect(getActionIconColorOrThrow('CODE')).toBe(
|
||||
getActionIconColorOrThrow('CODE'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+9
-13
@@ -1,37 +1,33 @@
|
||||
import { type WorkflowActionType } from '@/workflow/types/Workflow';
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
import { type ThemeType } from 'twenty-ui/theme';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
export const getActionIconColorOrThrow = ({
|
||||
theme,
|
||||
actionType,
|
||||
}: {
|
||||
theme: ThemeType;
|
||||
actionType: WorkflowActionType;
|
||||
}) => {
|
||||
export const getActionIconColorOrThrow = (
|
||||
actionType: WorkflowActionType,
|
||||
): string => {
|
||||
switch (actionType) {
|
||||
case 'CODE':
|
||||
case 'LOGIC_FUNCTION':
|
||||
case 'HTTP_REQUEST':
|
||||
case 'SEND_EMAIL':
|
||||
case 'DRAFT_EMAIL':
|
||||
return theme.color.red;
|
||||
return themeCssVariables.color.red;
|
||||
case 'CREATE_RECORD':
|
||||
case 'UPDATE_RECORD':
|
||||
case 'DELETE_RECORD':
|
||||
case 'UPSERT_RECORD':
|
||||
case 'FIND_RECORDS':
|
||||
return theme.font.color.tertiary;
|
||||
return themeCssVariables.font.color.tertiary;
|
||||
case 'FORM':
|
||||
return theme.color.orange;
|
||||
return themeCssVariables.color.orange;
|
||||
case 'ITERATOR':
|
||||
case 'EMPTY':
|
||||
case 'FILTER':
|
||||
case 'IF_ELSE':
|
||||
case 'DELAY':
|
||||
return theme.color.green12;
|
||||
return themeCssVariables.color.green12;
|
||||
case 'AI_AGENT':
|
||||
return theme.color.pink;
|
||||
return themeCssVariables.color.pink;
|
||||
default:
|
||||
assertUnreachable(actionType, `Unsupported action type: ${actionType}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user