Migrate twenty UI (#21407)
## Migrate all `twenty-ui-deprecated` components into `twenty-ui` Ports all **192 components** and **70 stories** into the new `twenty-ui` package with full public-API parity (export diff: 0 missing / 0 extra across all 13 modules; story titles byte-identical for the Argos cross-package diff). - **Styling:** Linaria → SCSS Modules, `var(--t-*)` tokens, `data-*` state. Canonical pattern in `Button.module.scss`. - **Behavior:** Base UI where mapped (Checkbox, Radio, Modal→Dialog, Tooltip drops `react-tooltip`, JSON tree→Collapsible); framer kept only where animation is the public contract. - **Fixed an inert axe gate** in `.storybook/vitest.setup.ts` (a11y addon annotations were never registered). Now live; 119 stories carry `a11y: 'todo'` overrides pending a fix pass.
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
.layout {
|
||||
background: var(--t-background-primary);
|
||||
border: 1px solid var(--t-border-color-light);
|
||||
border-radius: 5px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
max-width: calc(100% - 40px);
|
||||
padding: 20px;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
declare const classNames: {
|
||||
readonly layout: 'layout';
|
||||
};
|
||||
export default classNames;
|
||||
@@ -0,0 +1,32 @@
|
||||
import { isDefined } from '@ui/utilities/utils/isDefined';
|
||||
|
||||
import styles from './ComponentStorybookLayout.module.scss';
|
||||
|
||||
type ComponentStorybookLayoutProps = {
|
||||
width?: number;
|
||||
backgroundColor?: string | undefined;
|
||||
height?: number;
|
||||
children: JSX.Element;
|
||||
};
|
||||
|
||||
export const ComponentStorybookLayout = ({
|
||||
width,
|
||||
backgroundColor,
|
||||
height,
|
||||
children,
|
||||
}: ComponentStorybookLayoutProps) => {
|
||||
return (
|
||||
<div
|
||||
className={styles.layout}
|
||||
style={{
|
||||
// background falls back to the SCSS default when undefined
|
||||
background: backgroundColor,
|
||||
height: isDefined(height) ? `${height}px` : 'fit-content',
|
||||
minWidth: width ? 'unset' : '300px',
|
||||
width: width ? `${width}px` : 'fit-content',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
.columnTitle {
|
||||
font-size: var(--t-font-size-lg);
|
||||
font-weight: var(--t-font-weight-semi-bold);
|
||||
margin: var(--t-spacing-2);
|
||||
}
|
||||
|
||||
.rowsTitle {
|
||||
color: var(--t-font-color-secondary);
|
||||
font-size: var(--t-font-size-md);
|
||||
font-weight: var(--t-font-weight-semi-bold);
|
||||
margin: var(--t-spacing-2);
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.rowTitle {
|
||||
color: var(--t-font-color-tertiary);
|
||||
font-size: var(--t-font-size-md);
|
||||
font-weight: var(--t-font-weight-semi-bold);
|
||||
margin: var(--t-spacing-2);
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.elementTitle {
|
||||
color: var(--t-font-color-light);
|
||||
font-size: var(--t-font-size-xs);
|
||||
font-weight: var(--t-font-weight-semi-bold);
|
||||
margin-bottom: var(--t-spacing-1);
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.columnContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--t-spacing-2);
|
||||
}
|
||||
|
||||
.rowsContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--t-spacing-2);
|
||||
}
|
||||
|
||||
.rowContainer {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
gap: var(--t-spacing-2);
|
||||
}
|
||||
|
||||
.elementContainer {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.cellContainer {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--t-spacing-2);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
declare const classNames: {
|
||||
readonly columnTitle: 'columnTitle';
|
||||
readonly rowsTitle: 'rowsTitle';
|
||||
readonly rowTitle: 'rowTitle';
|
||||
readonly elementTitle: 'elementTitle';
|
||||
readonly container: 'container';
|
||||
readonly columnContainer: 'columnContainer';
|
||||
readonly rowsContainer: 'rowsContainer';
|
||||
readonly rowContainer: 'rowContainer';
|
||||
readonly elementContainer: 'elementContainer';
|
||||
readonly cellContainer: 'cellContainer';
|
||||
};
|
||||
export default classNames;
|
||||
@@ -0,0 +1,117 @@
|
||||
import { isNumber, isString } from '@sniptt/guards';
|
||||
import { type Decorator } from '@storybook/react-vite';
|
||||
import { clsx } from 'clsx';
|
||||
import { type ComponentProps, type JSX } from 'react';
|
||||
|
||||
import styles from './CatalogDecorator.module.scss';
|
||||
|
||||
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 (
|
||||
<div className={styles.container}>
|
||||
{dimension4.values.map((value4: any, index4: number) => (
|
||||
<div className={styles.columnContainer} key={`d4-${index4}`}>
|
||||
<h1 className={styles.columnTitle}>
|
||||
{dimension4.labels?.(value4) ??
|
||||
(isStringOrNumber(value4) ? value4 : '')}
|
||||
</h1>
|
||||
{dimension3.values.map((value3: any, index3: number) => (
|
||||
<div className={styles.rowsContainer} key={`d3-${index3}`}>
|
||||
<h2 className={styles.rowsTitle}>
|
||||
{dimension3.labels?.(value3) ??
|
||||
(isStringOrNumber(value3) ? value3 : '')}
|
||||
</h2>
|
||||
{dimension2.values.map((value2: any, index2: number) => (
|
||||
<div className={styles.rowContainer} key={`d2-${index2}`}>
|
||||
<h3 className={styles.rowTitle}>
|
||||
{dimension2.labels?.(value2) ??
|
||||
(isStringOrNumber(value2) ? value2 : '')}
|
||||
</h3>
|
||||
{dimension1.values.map((value1: any, index1: number) => {
|
||||
return (
|
||||
<div
|
||||
className={styles.cellContainer}
|
||||
key={`d1-${index1}`}
|
||||
id={value1}
|
||||
>
|
||||
<span className={styles.elementTitle}>
|
||||
{dimension1.labels?.(value1) ??
|
||||
(isStringOrNumber(value1) ? value1 : '')}
|
||||
</span>
|
||||
<div
|
||||
className={clsx(
|
||||
styles.elementContainer,
|
||||
options?.elementContainer?.className,
|
||||
)}
|
||||
style={{
|
||||
minWidth: options?.elementContainer?.width
|
||||
? `${options.elementContainer.width}px`
|
||||
: 'auto',
|
||||
...options?.elementContainer?.style,
|
||||
}}
|
||||
>
|
||||
<Story
|
||||
args={{
|
||||
...context.args,
|
||||
...dimension1.props(value1),
|
||||
...dimension2.props(value2),
|
||||
...dimension3.props(value3),
|
||||
...dimension4.props(value4),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
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: '/' }],
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import { type Decorator } from '@storybook/react-vite';
|
||||
|
||||
import { Provider as JotaiProvider } from 'jotai';
|
||||
|
||||
export const JotaiRootDecorator: Decorator = (Story) => (
|
||||
<JotaiProvider>
|
||||
<Story />
|
||||
</JotaiProvider>
|
||||
);
|
||||
@@ -0,0 +1,8 @@
|
||||
import { type Decorator } from '@storybook/react-vite';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
export const RouterDecorator: Decorator = (Story) => (
|
||||
<MemoryRouter>
|
||||
<Story />
|
||||
</MemoryRouter>
|
||||
);
|
||||
@@ -7,4 +7,20 @@
|
||||
* |___/
|
||||
*/
|
||||
|
||||
export {};
|
||||
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';
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
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=';
|
||||
@@ -0,0 +1,24 @@
|
||||
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'];
|
||||
};
|
||||
Reference in New Issue
Block a user