Rename twenty-ui to twenty-ui-deprecated and twenty-new-ui to twenty-ui to prepare package release (#21315)

## Description

Promotes the next-gen UI library (formerly `twenty-new-ui`) to the name
**`twenty-ui`** (v0.1.0, publishable) and renames the old package to
**`twenty-ui-deprecated`**. Rewrites ~1,730 `twenty-ui` imports →
`twenty-ui-deprecated`, updates all configs/CI/Docker/deps, and migrates
twenty-front's `Toggle` to the new package (first consumer) as a
drop-in.

## Next steps
- Wire the `ui/v*` publish dispatch (`cd-deploy-tag.yaml` +
`.yarnrc.yml`), then tag `ui/v0.1.0` to publish.
- Continue migrating components from `twenty-ui-deprecated` →
`twenty-ui`.
This commit is contained in:
Raphaël Bosi
2026-06-08 18:12:28 +02:00
committed by GitHub
parent a91e737e69
commit c596a5e342
2320 changed files with 6313 additions and 4856 deletions
@@ -1,52 +0,0 @@
import { styled } from '@linaria/react';
import { isDefined } from 'twenty-shared/utils';
import { themeCssVariables } from '@ui/theme-constants';
const StyledLayout = styled.div<{
width?: number;
backgroundColor?: string | undefined;
height: number | 'fit-content';
}>`
background: ${({ backgroundColor }) =>
backgroundColor ?? themeCssVariables.background.primary};
border: 1px solid ${themeCssVariables.border.color.light};
border-radius: 5px;
display: flex;
flex-direction: row;
height: ${({ height }) =>
height === 'fit-content'
? 'fit-content'
: `
${height}px
`};
max-width: calc(100% - 40px);
min-width: ${({ width }) => (width ? 'unset' : '300px')};
padding: 20px;
width: ${({ width }) => (width ? width + 'px' : 'fit-content')};
`;
type ComponentStorybookLayoutProps = {
width?: number;
backgroundColor?: string | undefined;
height?: number;
children: JSX.Element;
};
export const ComponentStorybookLayout = ({
width,
backgroundColor,
height,
children,
}: ComponentStorybookLayoutProps) => {
return (
<StyledLayout
width={width}
backgroundColor={backgroundColor}
height={isDefined(height) ? height : 'fit-content'}
>
{children}
</StyledLayout>
);
};
@@ -1,172 +0,0 @@
import { styled } from '@linaria/react';
import { isNumber, isString } from '@sniptt/guards';
import { type Decorator } from '@storybook/react-vite';
import { type ComponentProps, type JSX } from 'react';
import { themeCssVariables } from '@ui/theme-constants';
const StyledColumnTitle = styled.h1`
font-size: ${themeCssVariables.font.size.lg};
font-weight: ${themeCssVariables.font.weight.semiBold};
margin: ${themeCssVariables.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`
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`
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;
`;
const StyledContainer = styled.div`
display: flex;
flex-direction: row;
`;
const StyledColumnContainer = styled.div`
display: flex;
flex-direction: column;
padding: ${themeCssVariables.spacing[2]};
`;
const StyledRowsContainer = styled.div`
display: flex;
flex-direction: column;
gap: ${themeCssVariables.spacing[2]};
`;
const StyledRowContainer = styled.div`
display: flex;
flex: 1;
flex-direction: row;
gap: ${themeCssVariables.spacing[2]};
`;
const StyledElementContainer = styled.div<{ width: number }>`
display: flex;
min-width: ${({ width }) => (width ? `${width}px` : 'auto')};
`;
const StyledCellContainer = styled.div`
align-items: center;
display: flex;
flex-direction: column;
padding: ${themeCssVariables.spacing[2]};
`;
const emptyDimension = {
name: '',
values: [undefined],
props: () => ({}),
} as CatalogDimension;
const isStringOrNumber = (term: unknown): term is string | number =>
isString(term) || isNumber(term);
export type CatalogDimension<
ComponentType extends React.ElementType = () => JSX.Element,
> = {
name: string;
values: any[];
props: (value: any) => Partial<ComponentProps<ComponentType>>;
labels?: (value: any) => string;
};
export type CatalogOptions = {
elementContainer?: {
width?: number;
style?: React.CSSProperties;
className?: string;
};
};
export const CatalogDecorator: Decorator = (Story, context) => {
const {
catalog: { dimensions = [], options = {} } = {
dimensions: [],
options: {},
},
} = context.parameters || {};
if (!dimensions || !Array.isArray(dimensions)) {
return <Story />;
}
const [
dimension1 = emptyDimension,
dimension2 = emptyDimension,
dimension3 = emptyDimension,
dimension4 = emptyDimension,
] = dimensions as CatalogDimension[];
return (
<StyledContainer>
{dimension4.values.map((value4: any, index4: number) => (
<StyledColumnContainer key={`d4-${index4}`}>
<StyledColumnTitle>
{dimension4.labels?.(value4) ??
(isStringOrNumber(value4) ? value4 : '')}
</StyledColumnTitle>
{dimension3.values.map((value3: any, index3: number) => (
<StyledRowsContainer key={`d3-${index3}`}>
<StyledRowsTitle>
{dimension3.labels?.(value3) ??
(isStringOrNumber(value3) ? value3 : '')}
</StyledRowsTitle>
{dimension2.values.map((value2: any, index2: number) => (
<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}>
<StyledElementTitle>
{dimension1.labels?.(value1) ??
(isStringOrNumber(value1) ? value1 : '')}
</StyledElementTitle>
<StyledElementContainer
width={options?.elementContainer?.width}
style={options?.elementContainer?.style}
className={options?.elementContainer?.className}
>
<Story
args={{
...context.args,
...dimension1.props(value1),
...dimension2.props(value2),
...dimension3.props(value3),
...dimension4.props(value4),
}}
/>
</StyledElementContainer>
</StyledCellContainer>
);
})}
</StyledRowContainer>
))}
</StyledRowsContainer>
))}
</StyledColumnContainer>
))}
</StyledContainer>
);
};
@@ -1,40 +0,0 @@
import { type Decorator } from '@storybook/react-vite';
import { themeCssVariables } from '@ui/theme-constants';
import { ComponentStorybookLayout } from '../ComponentStorybookLayout';
const getBackgroundColor = (
inverted: boolean,
accent: string,
): string | undefined => {
if (!inverted) return undefined;
switch (accent) {
case 'default':
return themeCssVariables.grayScale.gray11;
case 'danger':
return themeCssVariables.color.red;
case 'blue':
return themeCssVariables.color.blue;
default:
return undefined;
}
};
export const ComponentDecorator: Decorator = (Story, context) => {
const { container } = context.parameters;
const inverted = context.args.inverted as boolean;
const accent = context.args.accent as string;
const backgroundColor = getBackgroundColor(inverted, accent);
return (
<ComponentStorybookLayout
width={container?.width}
height={container?.height}
backgroundColor={backgroundColor}
>
<Story />
</ComponentStorybookLayout>
);
};
@@ -1,82 +0,0 @@
import { type Decorator } from '@storybook/react-vite';
import {
createMemoryRouter,
createRoutesFromElements,
Outlet,
Route,
RouterProvider,
} from 'react-router-dom';
import { ComponentStorybookLayout } from '../ComponentStorybookLayout';
interface StrictArgs {
[name: string]: unknown;
}
export type RouteParams = {
[param: string]: string;
};
export const isRouteParams = (obj: any): obj is RouteParams => {
if (typeof obj !== 'object' || obj === null) {
return false;
}
return Object.keys(obj).every((key) => typeof obj[key] === 'string');
};
export const computeLocation = (
routePath: string,
routeParams?: RouteParams,
) => {
return {
pathname: routePath.replace(
/:(\w+)/g,
(paramName) => routeParams?.[paramName] ?? '',
),
};
};
const Providers = () => (
<ComponentStorybookLayout>
<Outlet />
</ComponentStorybookLayout>
);
const createRouter = ({
Story,
args,
initialEntries,
initialIndex,
}: {
Story: () => JSX.Element;
args: StrictArgs;
initialEntries?: {
pathname: string;
}[];
initialIndex?: number;
}) =>
createMemoryRouter(
createRoutesFromElements(
<Route element={<Providers />}>
<Route path={(args.routePath as string) ?? '*'} element={<Story />} />
</Route>,
),
{ initialEntries, initialIndex },
);
export const ComponentWithRouterDecorator: Decorator = (Story, { args }) => {
return (
<RouterProvider
router={createRouter({
Story,
args,
initialEntries:
args.routePath &&
typeof args.routePath === 'string' &&
(args.routeParams === undefined || isRouteParams(args.routeParams))
? [computeLocation(args.routePath, args.routeParams)]
: [{ pathname: '/' }],
})}
/>
);
};
@@ -1,9 +0,0 @@
import { type Decorator } from '@storybook/react-vite';
import { Provider as JotaiProvider } from 'jotai';
export const JotaiRootDecorator: Decorator = (Story) => (
<JotaiProvider>
<Story />
</JotaiProvider>
);
@@ -1,8 +0,0 @@
import { type Decorator } from '@storybook/react-vite';
import { MemoryRouter } from 'react-router-dom';
export const RouterDecorator: Decorator = (Story) => (
<MemoryRouter>
<Story />
</MemoryRouter>
);
+1 -17
View File
@@ -7,20 +7,4 @@
* |___/
*/
export { ComponentStorybookLayout } from './ComponentStorybookLayout';
export type {
CatalogDimension,
CatalogOptions,
} from './decorators/CatalogDecorator';
export { CatalogDecorator } from './decorators/CatalogDecorator';
export { ComponentDecorator } from './decorators/ComponentDecorator';
export type { RouteParams } from './decorators/ComponentWithRouterDecorator';
export {
isRouteParams,
computeLocation,
ComponentWithRouterDecorator,
} from './decorators/ComponentWithRouterDecorator';
export { JotaiRootDecorator } from './decorators/JotaiRootDecorator';
export { RouterDecorator } from './decorators/RouterDecorator';
export { AVATAR_URL_MOCK } from './mocks/avatarUrlMock';
export type { CatalogStory } from './types/CatalogStory';
export {};
@@ -1,2 +0,0 @@
export const AVATAR_URL_MOCK =
'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAYABgAAD/4QCMRXhpZgAATU0AKgAAAAgABQESAAMAAAABAAEAAAEaAAUAAAABAAAASgEbAAUAAAABAAAAUgEoAAMAAAABAAIAAIdpAAQAAAABAAAAWgAAAAAAAABgAAAAAQAAAGAAAAABAAOgAQADAAAAAQABAACgAgAEAAAAAQAAABSgAwAEAAAAAQAAABQAAAAA/8AAEQgAFAAUAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/bAEMACwgICggHCwoJCg0MCw0RHBIRDw8RIhkaFBwpJCsqKCQnJy0yQDctMD0wJyc4TDk9Q0VISUgrNk9VTkZUQEdIRf/bAEMBDA0NEQ8RIRISIUUuJy5FRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRf/dAAQAAv/aAAwDAQACEQMRAD8Ava1q728otYY98joSCTgZrnbXWdTtrhrfVZXWLafmcAEkdgR/hVltQku9Q8+OIEBcGOT+ID0PY1ka1KH2u8ToqnPLbmIqG7u6LtbQ7RXBRec4Uck9eKXcPWsKDWVnhWSL5kYcFelSf2m3901POh8jP//QoyIAnTuKpXsY82NsksUyWPU5q/L9z8RVK++/F/uCsVsaEURwgA4HtT9x9TUcf3KfUGh//9k=';
@@ -1,24 +0,0 @@
import { type StoryObj } from '@storybook/react-vite';
import { type ElementType } from 'react';
import {
type CatalogDimension,
type CatalogOptions,
} from '../decorators/CatalogDecorator';
export type CatalogStory<
StoryType extends StoryObj<ComponentType>,
ComponentType extends ElementType,
> = {
args?: StoryType['args'];
argTypes?: StoryType['argTypes'];
play?: StoryType['play'];
render?: StoryType['render'];
parameters: StoryType['parameters'] & {
catalog: {
dimensions: CatalogDimension<ComponentType>[];
options?: CatalogOptions;
};
};
decorators: StoryType['decorators'];
};