Skip to main content

convert-webcore-component

Converts webcore-style React components to Simorgh coding standards using SCSS Modules and TypeScript

Informações da origem

Repositório
bbc/simorgh
Última atividade na origem
29 de setembro de 2026 às 09:56
Idioma detectado do SKILL.md
inglês
Estrelas
1.718
Forks
280

Opções de instalação

Por padrão, está selecionado o prompt que primeiro revisa a origem. Você pode mudar para um comando direto ou baixar uma cópia local.

Revise os arquivos de origem

Leia o SKILL.md e os arquivos complementares exibidos pelo SkillsMP antes de decidir se vai instalar.

Exibindo SKILL.md

SKILL.md
Instruções da origem · Visualização somente leitura
name
convert-webcore-component
description
Converts webcore-style React components to Simorgh coding standards using SCSS Modules and TypeScript
# Convert Webcore Component Converts webcore-style React components (using `@emotion/styled` or `styled-components`) into the Simorgh coding standard, which uses **SCSS Modules** (`index.module.scss` + `className`) and TypeScript. Styling rules are defined in `.github/instructions/styling-standards.instructions.md` and auto-apply to `src/app/components/**`. This skill covers the conversion workflow only. ## Step 1: Pre-Conversion Checklist Before converting, audit imports for missing dependencies. **If dependencies are missing, stop and request they be imported first.** ### @bbc/web-components imports - `Carousel` - horizontal scrollable container with heading - `Heading` - has equivalent at `src/app/components/Heading/` ### @bbc/web-gel-layouts imports - `Wrap` - replace with div + `theme.$spacings-*` - `Grid` - may need custom CSS Grid implementation ### @bbc/web-gel-foundations imports - `SPACING_*` → `theme.$spacings-*` - `GROUP_*` → `theme.$mediaQueries-group-*` - `fontScale*` + `fontStandard` → `theme.typography-from-scale-and-variant(<scale>, '<variant>')` (use `getTypographyCustomProperties` + `theme.typography-from-custom-properties` instead when size/variant are runtime props — see [migrate-component-to-scss-modules](../migrate-component-to-scss-modules/SKILL.md#runtime-typography-sizevariant-known-only-from-props)) - `createSize` → `theme.pixelsToRem-px-to-rem()` ## Step 2: Convert Styles ### Before (webcore style) ```jsx import styled from '@emotion/styled'; const StyledWrapper = styled.div` display: flex; padding: 16px; @media (min-width: 600px) { padding: 24px; } `; const Component = () => <StyledWrapper>Content</StyledWrapper>; ``` ### After (Simorgh style) ```tsx import styles from './index.module.scss'; const Component = () => <div className={styles.wrapper}>Content</div>; ``` ```scss // index.module.scss @use '@scss/themeTokens' as theme; .wrapper { display: flex; padding: #{theme.$spacings-double}; @media #{theme.$mediaQueries-group-3-min-width} { padding: #{theme.$spacings-triple}; } } ``` ### Styles Using Theme Values Import `@scss/themeTokens` and reference its values in your SCSS rules. It provides the colours, spacings, breakpoints and typography for the active service, so Emotion's theme callback is no longer needed. **Before:** ```typescript container: ({ mq, palette }: Theme) => css({ backgroundColor: palette.WHITE, [mq.GROUP_3_MIN_WIDTH]: { padding: '1rem', }, }), ``` **After:** ```scss .container { background-color: theme.$palette-white; @media #{theme.$mediaQueries-group-3-min-width} { padding: #{theme.$spacings-double}; } } ``` ### Variant Styles A style that took a parameter becomes a second class holding only the properties that differ, applied in addition to the base class. **Before:** ```typescript link: (hasMargin?: boolean) => css({ color: palette.GREY_8, ...(hasMargin && { marginInlineStart: '1rem' }), }), ``` **After:** ```scss .link { color: theme.$palette-grey-8; } .linkWithMargin { margin-inline-start: #{theme.$spacings-double}; } ``` ```tsx <a className={clsx(styles.link, hasMargin && styles.linkWithMargin)}> ``` If the variant **replaces** the same properties rather than adding new ones, don't apply both classes. Two classes setting identical properties makes the result depend on their order in the compiled CSS, and leaves the base declarations unused. Use one class or the other. **Before:** ```typescript title: (isLarge?: boolean) => css({ fontSize: isLarge ? '2rem' : '1rem', }), ``` **After:** ```scss .title { @include theme.fontSizes-gel-font-size(pica); } .titleLarge { @include theme.fontSizes-gel-font-size(canon); } ``` ```tsx <h2 className={isLarge ? styles.titleLarge : styles.title}>{title}</h2> ``` This is distinct from the anti-pattern in the styling standards. A **discrete variant prop** (a boolean or a small union) can legitimately select or add a class. What to avoid is deriving a class from a **continuous or computed value** (`height > 100 ? styles.tall : styles.short`) or from `dir` — use the custom property or logical-property approaches below instead. ### Consumer-owned style overrides If a child component has a consumer-specific style override, use the DOM context the consumer already owns rather than relying on class order: ```scss .errorLinkWrapper { .inlineLink:not(:visited):not(:hover):not(:focus) { color: theme.$palette-black; border-bottom: #{theme.pixelsToRem-px-to-rem(1)} solid theme.$palette-black; } } ``` CSS Modules scope class names but do not give a React parent automatic precedence. The order of class names in HTML does not control the cascade. Prefer an ancestor selector or an element-qualified selector when the component owns the element type (for example `p.copyright`). Do not add a wrapper solely to manufacture specificity. Exclude pseudo-states that remain owned by the child component. Use duplicated selectors only as a documented last resort when no meaningful DOM context exists. ### Combining Class Names Emotion's `css` array silently ignores falsy entries. `className` is a plain string and does not, so translating the array directly introduces a bug. Note that the condition is a **prop**, not the style object — `styles.linkWithMargin` always exists and is always truthy. Most components name the two differently (`isActive && styles.tabActive`), but `CallToActionLink` uses `alignWithMargin && styles.alignWithMargin`, which is easy to misread as a single reference. **Before:** ```tsx <a css={[styles.link, hasMargin && styles.linkWithMargin]}> ``` **After:** ```tsx import clsx from 'clsx'; <a className={clsx(styles.link, hasMargin && styles.linkWithMargin)}> ``` `clsx` drops falsy entries the same way Emotion's `css` array did, so this is a direct translation rather than a rewrite. ❌ A plain template literal carries `&&` across as the literal string `false`: ```tsx // produces class="link_x1 false" when hasMargin is false <a className={`${styles.link} ${hasMargin && styles.linkWithMargin}`}> ``` Use `clsx` for any conditional combination of classes, however many branches are involved. ### Dynamic Styles SCSS Module classes are static. Where a value genuinely varies per instance, set a single CSS custom property inline and consume it in SCSS. **Before:** ```jsx const Button = styled.div` padding-${({ alignment }) => alignment === 'left' ? 'right' : 'left'}: 12px; `; ``` **After** — prefer a logical property so no branching is needed at all: ```scss .button { padding-inline-end: #{theme.pixelsToRem-px-to-rem(12)}; } ``` Where a value is truly dynamic (a computed height, an image offset): ```tsx <div className={styles.wrapper} style={{ '--promo-height': `${height}px` }} /> ``` ```scss .wrapper { height: var(--promo-height); } ``` ### Props with many possible values When a prop has many possible values, avoid creating a separate class and CSS rule for every value unless the list is small. The browser downloads all of those rules, even though each instance uses only one value. For a small list, keep the value in a `data-*` attribute and select the matching rule in SCSS: ```tsx <a className={styles.link} data-size={size} /> ``` ```scss .link[data-size='small'] { font-size: 1rem; } ``` For a larger list, use one set of responsive rules and pass the selected values as CSS custom properties: ```tsx import type { CSSProperties } from 'react'; type LinkSize = 'small' | 'large'; type LinkStyles = CSSProperties & { '--link-font-size': string; '--link-line-height': string; }; const getStylesForSize = (size: LinkSize): LinkStyles => ({ '--link-font-size': size === 'small' ? '1rem' : '1.25rem', '--link-line-height': size === 'small' ? '1.5' : '1.25', }); <a className={styles.link} style={getStylesForSize(size)} /> ``` ```scss .link { font-size: var(--link-font-size, inherit); line-height: var(--link-line-height, inherit); } ``` Keep the code that maps prop values to custom-property values in one typed helper or at the component boundary. Do not repeat that mapping in both TypeScript and SCSS. When the component renders in AMP, measure the final inlined `style[amp-custom]` payload and keep the total below AMP's hard 75 KB limit. This is an additional AMP requirement; the CSS-minimization guidance applies to all editions. ### Styling Mistakes to Avoid - Don't produce `index.styles.ts` files or use Emotion's `css` prop / `styled` API - Don't use physical CSS properties (`left`, `right`) for directional layouts - Don't hardcode pixel, colour or breakpoint values — use theme tokens - Don't use `max-width` media queries when `min-width` would work - Don't derive a class name from a continuous/computed value or from `dir` (see above) ## Step 3: Convert to TypeScript ### File Extensions - `.jsx` → `.tsx` (React components) - `.js` → `.ts` (utilities, helpers, enums) ### File Structure ``` components/ ├── index.module.scss # Co-located SCSS Module styles ├── types.ts # Shared TypeScript types ├── ComponentA.tsx # React component ├── ComponentB.tsx # React component └── sub-component/ └── index.tsx ``` ### Props Type Definitions ```tsx interface MyComponentProps { name: string; count: number; isActive?: boolean; } const MyComponent = ({ name, count, isActive = false }: MyComponentProps) => { // ... }; ``` ### Import Existing Types ```tsx import type { HeadToHeadV2Data, Team, Action } from './types'; interface Props { data: HeadToHeadV2Data; isConciseView: boolean; } ``` ### Children Props ```tsx import type { PropsWithChildren, ReactNode } from 'react'; interface WrapperProps { className?: string; } const Wrapper = ({ children, className }: PropsWithChildren<WrapperProps>) => ( <div className={className}>{children}</div> ); ``` ### Event Handlers ```tsx import type { MouseEvent, ChangeEvent } from 'react'; interface ButtonProps { onClick?: (event: MouseEvent<HTMLButtonElement>) => void; onChange?: (event: ChangeEvent<HTMLInputElement>) => void; } ``` ### Enum Conversions ```ts export const EventStatus = { PreEvent: 'PreEvent', MidEvent: 'MidEvent', PostEvent: 'PostEvent', } as const; export type EventStatusType = typeof EventStatus[keyof typeof EventStatus]; ``` ### Conditional Props ```tsx type ConditionalLinkProps = | { href: string; onClick?: never } | { href?: never; onClick: () => void }; interface BaseLinkProps { children: ReactNode; className?: string; } type LinkProps = BaseLinkProps & ConditionalLinkProps; ``` ### TypeScript Mistakes to Avoid - Don't use `any` type - prefer `unknown` or proper types - Don't forget to update imports when renaming `.jsx` to `.tsx` - Don't use `React.FC` - use explicit return types or inference - Don't duplicate types - import from existing `types.ts` - Don't use non-null assertions (`!`) - prefer optional chaining (`?.`) - Don't leave JSDoc annotations after converting to TypeScript ## Step 4: Linting and Formatting After conversion, run linting and formatting to ensure code quality: ```bash # Run ESLint to fix issues yarn lint --fix # Run Prettier to format code yarn prettier --write "path/to/converted/files/**/*.{ts,tsx}" ``` ### Common Lint Issues After Conversion - Missing return types on functions - Unused imports from old styled-components - Incorrect import order (types should use `import type`) - Missing semicolons or trailing commas ## Reference Examples - [src/app/components/ArticleLinksBlock/index.module.scss](../../../src/app/components/ArticleLinksBlock/index.module.scss)
Ver no GitHub
Este SKILL.md e muito grande, entao o SkillsMP mostra aqui apenas a primeira secao. Ver no GitHub