[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:
Charles Bochet
2026-03-04 23:30:25 +01:00
committed by GitHub
parent 4c001778c2
commit c41a8e2b23
61 changed files with 4580 additions and 3244 deletions
@@ -18,12 +18,12 @@ import { getActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-ac
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
import { getTriggerIconColor } from '@/workflow/workflow-trigger/utils/getTriggerIconColor';
import { t } from '@lingui/core/macro';
import { useContext, useState } from 'react';
import { useState } from 'react';
import { CommandMenuPages, CoreObjectNameSingular } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { TRIGGER_STEP_ID } from 'twenty-shared/workflow';
import { useIcons } from 'twenty-ui/display';
import { ThemeContext } from 'twenty-ui/theme';
import { ICON_SIZES, ICON_STROKES } from 'twenty-ui/theme-constants';
import { CommandMenuPageInfoLayout } from './CommandMenuPageInfoLayout';
export const CommandMenuWorkflowStepInfo = ({
@@ -31,7 +31,6 @@ export const CommandMenuWorkflowStepInfo = ({
}: {
commandMenuPageInstanceId: string;
}) => {
const { theme } = useContext(ThemeContext);
const { getIcon } = useIcons();
const commandMenuPage = useAtomStateValue(commandMenuPageState);
@@ -121,14 +120,8 @@ export const CommandMenuWorkflowStepInfo = ({
: getActionIcon(stepDefinition.definition.type);
const headerIconColor = isTrigger
? getTriggerIconColor({
theme,
triggerType: stepDefinition.definition.type,
})
: getActionIconColorOrThrow({
theme,
actionType: stepDefinition.definition.type,
});
? getTriggerIconColor(stepDefinition.definition.type)
: getActionIconColorOrThrow(stepDefinition.definition.type);
const headerType = isTrigger ? t`Trigger` : t`Action`;
@@ -176,7 +169,7 @@ export const CommandMenuWorkflowStepInfo = ({
<CommandMenuPageInfoLayout
icon={
headerIcon ? (
<Icon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
<Icon size={ICON_SIZES.md} stroke={ICON_STROKES.sm} />
) : undefined
}
iconColor={headerIconColor}
@@ -21,15 +21,13 @@ import { CommandMenuPages, CoreObjectNameSingular } from 'twenty-shared/types';
import { useRunWorkflowRunOpeningInCommandMenuSideEffects } from '@/workflow/hooks/useRunWorkflowRunOpeningInCommandMenuSideEffects';
import { t } from '@lingui/core/macro';
import { useStore } from 'jotai';
import { useCallback, useContext } from 'react';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { useIcons } from 'twenty-ui/display';
import { v4 } from 'uuid';
import { ThemeContext } from 'twenty-ui/theme';
export const useOpenRecordInCommandMenu = () => {
const store = useStore();
const { theme } = useContext(ThemeContext);
const { getIcon } = useIcons();
const { navigateCommandMenu } = useCommandMenu();
@@ -168,10 +166,9 @@ export const useOpenRecordInCommandMenu = () => {
? getIcon(objectMetadataItem.icon)
: getIcon('IconList');
const IconColor = getIconColorForObjectType({
objectType: objectMetadataItem.nameSingular,
theme,
});
const IconColor = getIconColorForObjectType(
objectMetadataItem.nameSingular,
);
const objectLabelSingular = objectMetadataItem.labelSingular;
@@ -210,7 +207,6 @@ export const useOpenRecordInCommandMenu = () => {
navigateCommandMenu,
openNewRecordTitleCell,
runWorkflowRunOpeningInCommandMenuSideEffects,
theme,
store,
],
);
@@ -5,9 +5,8 @@ import { SelectableListItem } from '@/ui/layout/selectable-list/components/Selec
import { styled } from '@linaria/react';
import { ColorSample } from 'twenty-ui/display';
import { MenuItemSelect } from 'twenty-ui/navigation';
import { type ThemeColor, ThemeContext } from 'twenty-ui/theme';
import { type ThemeColor } from 'twenty-ui/theme';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { useContext } from 'react';
type ChartColorGradientOptionProps = {
colorOption: {
@@ -33,8 +32,8 @@ export const ChartColorGradientOption = ({
onSelectColor,
}: ChartColorGradientOptionProps) => {
const colorName = colorOption.colorName as ThemeColor;
const { theme } = useContext(ThemeContext);
const colorRegistry = createGraphColorRegistry(theme);
const colorRegistry = createGraphColorRegistry();
const colorSamples = (
<StyledColorSamplesContainer>
@@ -6,10 +6,9 @@ import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
import { ColorSample } from 'twenty-ui/display';
import { MenuItemSelect } from 'twenty-ui/navigation';
import { type ThemeColor, ThemeContext } from 'twenty-ui/theme';
import { type ThemeColor } from 'twenty-ui/theme';
import { getMainColorNameFromPaletteColorName } from 'twenty-ui/utilities';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { useContext } from 'react';
type ChartColorPaletteOptionProps = {
selectedItemId: string | null;
@@ -28,9 +27,7 @@ export const ChartColorPaletteOption = ({
currentColor,
onSelectColor,
}: ChartColorPaletteOptionProps) => {
const { theme } = useContext(ThemeContext);
const colorRegistry = createGraphColorRegistry(theme);
const colorRegistry = createGraphColorRegistry();
const paletteColors = Array.from(
{ length: CHART_SETTINGS_PALETTE_COLOR_GROUP_COUNT },
@@ -15,8 +15,6 @@ import { useLingui } from '@lingui/react/macro';
import { IconFunction } from 'twenty-ui/display';
import { MenuItem } from 'twenty-ui/navigation';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
import { useContext } from 'react';
import { ThemeContext } from 'twenty-ui/theme';
export type WorkflowActionSelection = {
type: WorkflowActionType;
@@ -32,7 +30,6 @@ export const CommandMenuWorkflowSelectAction = ({
const isDraftEmailEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_DRAFT_EMAIL_ENABLED,
);
const { theme } = useContext(ThemeContext);
const { t } = useLingui();
@@ -114,10 +111,7 @@ export const CommandMenuWorkflowSelectAction = ({
withIconContainer={true}
LeftIcon={() => (
<IconFunction
color={getActionIconColorOrThrow({
theme,
actionType: 'LOGIC_FUNCTION',
})}
color={getActionIconColorOrThrow('LOGIC_FUNCTION')}
size={16}
/>
)}
@@ -2,8 +2,6 @@ import { type WorkflowActionType } from '@/workflow/types/Workflow';
import { getActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIconColorOrThrow';
import { useIcons } from 'twenty-ui/display';
import { MenuItem } from 'twenty-ui/navigation';
import { useContext } from 'react';
import { ThemeContext } from 'twenty-ui/theme';
type Action = {
defaultLabel: string;
@@ -18,7 +16,6 @@ export const WorkflowActionMenuItems = ({
actions: Action[];
onClick: (actionType: WorkflowActionType) => void;
}) => {
const { theme } = useContext(ThemeContext);
const { getIcon } = useIcons();
return (
@@ -31,13 +28,7 @@ export const WorkflowActionMenuItems = ({
withIconContainer={true}
key={action.type}
LeftIcon={() => (
<Icon
color={getActionIconColorOrThrow({
theme,
actionType: action.type,
})}
size={16}
/>
<Icon color={getActionIconColorOrThrow(action.type)} size={16} />
)}
text={action.defaultLabel}
onClick={() => onClick(action.type)}