Skip to main content

convert-webcore-component

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

소스 정보

저장소
bbc/simorgh
최근 소스 활동
2026년 9월 29일 09:56
감지된 SKILL.md 언어
영어
스타
1,718
포크
280

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
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)
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기