ワンクリックで
convert-webcore-component
// Converts webcore-style React components to Simorgh coding standards using Emotion's css prop pattern
// Converts webcore-style React components to Simorgh coding standards using Emotion's css prop pattern
| name | convert-webcore-component |
| description | Converts webcore-style React components to Simorgh coding standards using Emotion's css prop pattern |
Converts webcore-style React components (using @emotion/styled or styled-components) into the Simorgh coding standard which uses Emotion's css prop pattern.
Before converting, audit imports for missing dependencies. If dependencies are missing, stop and request they be imported first.
Carousel - horizontal scrollable container with headingHeading - has equivalent at src/app/components/Heading/Wrap - replace with div + theme spacingsGrid - may need custom CSS Grid implementationSPACING_* → Use theme spacingsGROUP_* → Use theme mq media queriesfontScale*, fontStandard → Use theme typographycreateSize → Use pixelsToRem()import styled from '@emotion/styled';
const StyledWrapper = styled.div`
display: flex;
padding: 16px;
@media (min-width: 600px) {
padding: 24px;
}
`;
const Component = () => <StyledWrapper>Content</StyledWrapper>;
import styles from './index.styles';
const Component = () => <div css={styles.wrapper()}>Content</div>;
import { css } from '@emotion/react';
import pixelsToRem from '../../utilities/pixelsToRem';
export default {
wrapper: () =>
css({
display: 'flex',
padding: `${pixelsToRem(16)}rem`,
[`@media (min-width: ${pixelsToRem(600)}rem)`]: {
padding: `${pixelsToRem(24)}rem`,
},
}),
// Style with parameters
title: (isLarge?: boolean) =>
css({
fontSize: isLarge ? '2rem' : '1rem',
}),
// Style using theme
container: ({ mq, palette }: Theme) =>
css({
backgroundColor: palette.WHITE,
[mq.GROUP_3_MIN_WIDTH]: {
padding: '1rem',
},
}),
};
Before:
const Button = styled.div`
padding-${({ alignment }) => alignment === 'left' ? 'right' : 'left'}: 12px;
`;
After:
button: (alignment: 'left' | 'right') =>
css({
...(alignment === 'left'
? { paddingInlineEnd: `${pixelsToRem(12)}rem` }
: { paddingInlineStart: `${pixelsToRem(12)}rem` }),
}),
Use logical CSS properties for LTR/RTL support:
paddingInlineStart instead of padding-leftmarginBlockEnd instead of margin-bottomborderInlineStart instead of border-leftUse mobile-first media queries with min-width
Use pixelsToRem utility for pixel-to-rem conversion
Group styles by component area in the styles file
Export GRID_AREAS constants from styles if used in multiple components:
export const GRID_AREAS = {
homeText: 'home_text',
awayText: 'away_text',
} as const;
Style functions always return css() call:
wrapper: () => css({ display: 'flex' }),
Use arrays for composable styles:
keyEventsHome: () => [
baseStyles,
css({ textAlign: 'end' }),
],
left, right) for directional layoutspixelsToRem()max-width media queries when min-width would work.jsx → .tsx (React components).js → .ts (utilities, helpers, enums)components/
├── index.styles.ts # Consolidated styles
├── types.ts # Shared TypeScript types
├── ComponentA.tsx # React component
├── ComponentB.tsx # React component
└── sub-component/
└── index.tsx
interface MyComponentProps {
name: string;
count: number;
isActive?: boolean;
}
const MyComponent = ({ name, count, isActive = false }: MyComponentProps) => {
// ...
};
import type { HeadToHeadV2Data, Team, Action } from './types';
interface Props {
data: HeadToHeadV2Data;
isConciseView: boolean;
}
import type { PropsWithChildren, ReactNode } from 'react';
interface WrapperProps {
className?: string;
}
const Wrapper = ({ children, className }: PropsWithChildren<WrapperProps>) => (
<div className={className}>{children}</div>
);
import type { MouseEvent, ChangeEvent } from 'react';
interface ButtonProps {
onClick?: (event: MouseEvent<HTMLButtonElement>) => void;
onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
}
export const EventStatus = {
PreEvent: 'PreEvent',
MidEvent: 'MidEvent',
PostEvent: 'PostEvent',
} as const;
export type EventStatusType = typeof EventStatus[keyof typeof EventStatus];
type ConditionalLinkProps =
| { href: string; onClick?: never }
| { href?: never; onClick: () => void };
interface BaseLinkProps {
children: ReactNode;
className?: string;
}
type LinkProps = BaseLinkProps & ConditionalLinkProps;
any type - prefer unknown or proper types.jsx to .tsxReact.FC - use explicit return types or inferencetypes.ts!) - prefer optional chaining (?.)After conversion, run linting and formatting to ensure code quality:
# Run ESLint to fix issues
yarn lint --fix
# Run Prettier to format code
yarn prettier --write "path/to/converted/files/**/*.{ts,tsx}"
import type)When migrating incrementally, you may want to keep the original .jsx/.js files alongside new .tsx/.ts files:
.tsx/.ts files with the same names.jsx/.js files remain untouched.ts/.tsx over .js/.jsxWhen both .jsx and .tsx versions exist, create a components/index.ts barrel file to ensure TypeScript imports resolve correctly:
// components/index.ts
export { ActionGrid, GRID_AREAS } from './action-grid';
export { default as Footer } from './footer';
export { HeadToHeadBanner } from './head-to-head-banner';
export { default as HeadToHeadHeader } from './head-to-head-header';
// ... other exports
Then import from the barrel file in parent components:
// head-to-head-v2.tsx
import {
Footer,
HeadToHeadHeader,
HeadToHeadBanner,
Actions,
} from './components';
When converting, ensure internal types are exported from types.ts:
// types.ts - Make sure to export types needed by child components
export type Action = { /* ... */ };
export type PlayerActions = { /* ... */ };
export type RunningScores = { /* ... */ };
export type EventStatusType = 'PreEvent' | 'MidEvent' | 'PostEvent' | /* ... */;
Preserve or add eslint disable comments where the original code had them:
/* eslint-disable jsx-a11y/aria-role */
/* eslint-disable import/prefer-default-export */
Avoid:
{hasGroupedEvents && (
<GroupedEvents groupedEvents={data.groupedActions!} />
)}
Prefer:
{hasGroupedEvents && data.groupedActions && (
<GroupedEvents groupedEvents={data.groupedActions} />
)}
Record Types for Object Mappingsconst goalTypesHandled: Record<string, string> = {
Penalty: 'pen',
'Own Goal': 'og',
};
const MATCH_STATUS_LETTERS: Record<string, string> = {
Postponed: 'P',
Cancelled: 'C',
};
@jsxImportSource Pragma NOT Needed in SimorghSimorgh's tsconfig.json already has "jsxImportSource": "@emotion/react" configured globally, so you do not need to add the pragma comment to individual files:
// NOT needed in Simorgh - already configured globally
/** @jsxImportSource @emotion/react */
If you're working in a different project without global configuration, you would need the pragma.
Instead of using enums, prefer union types for event status:
export type EventStatusType =
| 'PreEvent'
| 'MidEvent'
| 'PostEvent'
| 'Abandoned'
| 'Cancelled'
| 'Suspended'
| 'Postponed'
| 'Delayed'
| 'Intermission';
Define reusable types for common patterns:
export type BadgeSize =
| number
| { small?: number; medium?: number; large?: number };
export type Alignment = 'home' | 'away';
export type BadgePlaceholderFallbackType = 'badge' | 'flag';