[DevXP] Improve Linaria pre-build speed (#18382)

## Summary

This PR improves Linaria/WYW pre-build speed and continues the migration
of `twenty-ui` components away from runtime `ThemeContext` reads toward
static CSS variables and theme constants.

### Linaria/WYW profiling plugin improvements (`twenty-shared`)

- **Babel JIT warmup**: added a `buildStart` warmup step that triggers
WYW's Babel JIT compilation before the real build starts, so the first
real file doesn't pay the cold-start penalty
- **`configResolved` hook**: detects dev vs prod mode and resolves the
correct warmup file path relative to `config.root`
- **Dev-only per-file logging**: slow file warnings are now gated behind
`isDevMode`, keeping production/CI build output clean
- **`closeBundle` summary**: moved the final top-slow-files report to
`closeBundle` for accurate end-of-build reporting
- **Removed noisy progress interval logging** in favor of the warmup log
+ final summary

### Migration from `ThemeContext` to static CSS variables / constants

Across `twenty-ui`, replaced runtime `useTheme()` reads with:
- `themeCssVariables` CSS custom properties (colors, spacing)
- Hard-coded design-system constants (`ICON.size.md` → `16`,
`ICON.stroke.sm` → `1.6`) so components no longer need a React context
at render time — enabling Linaria static extraction

**Components migrated:**
- `Button`, `AnimatedButton`, `LightButton`, `LightIconButton`,
`AnimatedLightIconButton`, `ButtonIcon`, `ButtonSoon`
- `ProgressBar` (Framer Motion width animation → CSS `transition`)
- `Info`, `HorizontalSeparator`, `LinkChip`
- `MenuPicker`, `MenuItemLeftContent`, `MenuItemIconWithGripSwap`,
`NavigationBarItem`
- `JsonArrow`, `JsonNestedNode`
- `ModalHeader`

### Other
- Added `aria-valuenow` to `ProgressBar` for accessibility
- `VisibilityHidden` component updated to inline accessibility styles
This commit is contained in:
Charles Bochet
2026-03-04 17:04:16 +01:00
committed by GitHub
parent be01a85d67
commit eda905f271
55 changed files with 691 additions and 650 deletions
@@ -7,7 +7,6 @@ import { JsonNodeValue } from '@ui/json-visualizer/components/internal/JsonNodeV
import { JsonNode } from '@ui/json-visualizer/components/JsonNode';
import { useJsonTreeContextOrThrow } from '@ui/json-visualizer/hooks/useJsonTreeContextOrThrow';
import { type JsonNodeHighlighting } from '@ui/json-visualizer/types/JsonNodeHighlighting';
import { ANIMATION } from '@ui/theme';
import { themeCssVariables } from '@ui/theme-constants';
import { AnimatePresence, motion } from 'framer-motion';
import { useState } from 'react';
@@ -50,8 +49,6 @@ const StyledJsonListBase = styled.ul<{
}
`;
const StyledJsonList = motion.create(StyledJsonListBase);
export const JsonNestedNode = ({
label,
Icon,
@@ -80,45 +77,34 @@ export const JsonNestedNode = ({
);
const renderedChildren = (
<StyledJsonList
initial={{
height: 0,
opacity: 0,
overflowY: 'clip',
}}
animate={{
height: 'auto',
opacity: 1,
overflowY: 'clip',
}}
exit={{
height: 0,
opacity: 0,
overflowY: 'clip',
}}
transition={{ duration: ANIMATION.duration.normal }}
depth={depth}
<motion.div
initial={{ height: 0, opacity: 0, overflow: 'clip' }}
animate={{ height: 'auto', opacity: 1, overflow: 'clip' }}
exit={{ height: 0, opacity: 0, overflow: 'clip' }}
transition={{ duration: 0.3 }}
>
{elements.length === 0 ? (
<JsonNodeValue valueAsString={emptyElementsText} />
) : (
elements.map(({ id, label, value }) => {
const nextKeyPath = isNonEmptyString(keyPath)
? `${keyPath}.${id}`
: String(id);
<StyledJsonListBase depth={depth}>
{elements.length === 0 ? (
<JsonNodeValue valueAsString={emptyElementsText} />
) : (
elements.map(({ id, label, value }) => {
const nextKeyPath = isNonEmptyString(keyPath)
? `${keyPath}.${id}`
: String(id);
return (
<JsonNode
key={id}
label={label}
value={value}
depth={depth + 1}
keyPath={nextKeyPath}
/>
);
})
)}
</StyledJsonList>
return (
<JsonNode
key={id}
label={label}
value={value}
depth={depth + 1}
keyPath={nextKeyPath}
/>
);
})
)}
</StyledJsonListBase>
</motion.div>
);
const handleArrowClick = () => {
@@ -2,10 +2,8 @@ import { styled } from '@linaria/react';
import { VisibilityHidden } from '@ui/accessibility';
import { IconChevronDown } from '@ui/display';
import { useJsonTreeContextOrThrow } from '@ui/json-visualizer/hooks/useJsonTreeContextOrThrow';
import { ANIMATION, ThemeContext } from '@ui/theme';
import { themeCssVariables } from '@ui/theme-constants';
import { ICON_SIZES, themeCssVariables } from '@ui/theme-constants';
import { motion } from 'framer-motion';
import { useContext } from 'react';
const StyledButton = styled.button<{
variant?: 'blue' | 'red';
@@ -31,8 +29,6 @@ const StyledButton = styled.button<{
cursor: pointer;
`;
const MotionIconChevronDown = motion.create(IconChevronDown);
export const JsonArrow = ({
isOpen,
onClick,
@@ -42,30 +38,29 @@ export const JsonArrow = ({
onClick: () => void;
variant?: 'blue' | 'red';
}) => {
const { theme } = useContext(ThemeContext);
const { arrowButtonCollapsedLabel, arrowButtonExpandedLabel } =
useJsonTreeContextOrThrow();
const iconColor =
variant === 'blue'
? themeCssVariables.color.blue
: variant === 'red'
? themeCssVariables.font.color.danger
: themeCssVariables.font.color.secondary;
return (
<StyledButton variant={variant} onClick={onClick}>
<VisibilityHidden>
{isOpen ? arrowButtonExpandedLabel : arrowButtonCollapsedLabel}
</VisibilityHidden>
<MotionIconChevronDown
size={theme.icon.size.md}
color={
variant === 'blue'
? theme.color.blue
: variant === 'red'
? theme.font.color.danger
: theme.font.color.secondary
}
<motion.div
initial={false}
animate={{ rotate: isOpen ? 0 : -90 }}
transition={{ duration: ANIMATION.duration.normal }}
/>
transition={{ duration: 0.3 }}
>
<IconChevronDown size={ICON_SIZES.md} color={iconColor} />
</motion.div>
</StyledButton>
);
};