diff --git a/.claude/skills/deprecate-cds-api/SKILL.md b/.claude/skills/deprecate-cds-api/SKILL.md new file mode 100644 index 0000000000..8dbc95f9f6 --- /dev/null +++ b/.claude/skills/deprecate-cds-api/SKILL.md @@ -0,0 +1,152 @@ +--- +name: deprecate-cds-api +description: | + Deprecates a CDS component, hook, or other exported symbol with consistent JSDoc, version tags, + and docsite metadata across every public export path (web, mobile, common, visualization), not only + the original package. Use whenever the user asks to deprecate a CDS component or API, mark something + as deprecated, add @deprecated / @deprecationExpectedRemoval, or update deprecation warnings in + apps/docs metadata under components or hooks (webMetadata.json / mobileMetadata.json / metadata.json). + Also use when replacing a component or hook and sunsetting the old one. Always finish by running + `yarn nx run :lint` on modified packages so `internal/deprecated-jsdoc-has-removal-version` passes. +allowed-tools: Read, Grep, Glob, StrReplace, Bash(yarn nx run:*) +argument-hint: ' — replacement — [@deprecationExpectedRemoval major e.g. v10] — [optional notes]' +--- + +# Deprecate CDS public API + +Automate the standard CDS deprecation workflow for symbols exported from `packages/web`, `packages/mobile`, `packages/common`, `packages/web-visualization`, or `packages/mobile-visualization`. + +## Inputs to confirm first + +1. **What is being deprecated?** Component name, hook, prop, or other exported symbol. +2. **What should consumers use instead?** The replacement must be named in JSDoc and in docs `warning` text. +3. **Which major should `@deprecationExpectedRemoval` use?** (e.g. `v9`, `v10`.) **Ask the user to confirm** if they have not already stated it. If they want a default, **suggest** the next major from the relevant `package.json` (see Step 2) and confirm they accept it before editing. + +--- + +## Step 0 — Discover every public export (all packages) + +**Deprecate the symbol everywhere it is publicly reachable**, not only where it is first implemented. + +1. For each CDS package (`web`, `mobile`, `common`, `web-visualization`, `mobile-visualization`), trace the symbol from that package’s `package.json` **`exports`** map → barrel / `index` files → the module that declares or re-exports the symbol. +2. **`Grep`** for the symbol name under `packages//src` (e.g. `export { Foo`, `export * from`, `Foo as`) to catch re-exports and alternate entry paths. +3. **Every** package that publicly exports the symbol must end up with deprecation coverage: primary implementation **and** any re-export site where your tooling or consumers would not see JSDoc from the source file (add JSDoc on the re-export line or duplicate the tags as needed so imports from `@coinbase/cds-web`, `@coinbase/cds-mobile`, `@coinbase/cds-common`, etc. all surface the deprecation). + +Do **not** skip a package because the symbol is “originally” defined elsewhere—if consumers can import it from that package, it must be deprecated there too. + +--- + +## Step 1 — JSDoc on the deprecated symbol + +Add or extend JSDoc immediately above the deprecated export (component, function, type alias, const, interface field, etc.). + +Use the **standard JSDoc tag `@deprecated`** (not `@deprecate`). + +**Required shape:** + +```ts +/** + * …existing description if any… + * + * @deprecated . This will be removed in a future major release. + * @deprecationExpectedRemoval v + */ +``` + +Rules: + +- The `@deprecated` line must end with exactly: `This will be removed in a future major release.` (same sentence as the rest of the deprecation message, as in existing CDS examples). +- `@deprecationExpectedRemoval` must match `v` + version (e.g. `v9` or `v9.0.0`; full semver is allowed by ESLint). + +The repo’s ESLint rule **`internal/deprecated-jsdoc-has-removal-version`** (`libs/eslint-plugin-internal`) enforces the prose ending and the presence of `@deprecationExpectedRemoval`; **lint must pass** after edits (see **Step 6**). + +--- + +## Step 2 — Removal version for `@deprecationExpectedRemoval` + +The tag must satisfy `@deprecationExpectedRemoval v…` as enforced by ESLint (e.g. `v10` or `v10.0.0`). + +1. **Confirm with the user** which major **`N`** to use, unless they already specified it in **Inputs** (e.g. “remove in v10” → use `v10`). +2. **Default suggestion** when the user wants a recommendation: read the **`version`** field from the relevant `package.json` and set **`N = current major + 1`**. + - **`packages/web`**, **`packages/mobile`**, and **`packages/common`** always share the same semver — read **`version`** from any one of them (e.g. `8.60.0` → suggest **`v9`**). + - Symbols owned only by **`packages/web-visualization`** or **`packages/mobile-visualization`**: read **that** package’s `package.json` (those versions are independent from web/mobile/common). +3. After agreeing on **`N`**, use **`@deprecationExpectedRemoval v`** everywhere for this deprecation (same **Step 3**). + +Do **not** assume the default without checking—either the user names **`N`**, or they accept the suggested next-major after you show the current **`version`**. + +--- + +## Step 3 — Consistency across export surfaces + +- Use the **same** `@deprecated` guidance and **`@deprecationExpectedRemoval v`** value everywhere the symbol is exported (adjust wording only if a platform’s API genuinely differs). +- **Components** and **hooks** that exist as separate web and mobile implementations: deprecate **both** when both packages export the symbol. +- **Shared** symbols in `packages/common` that are **also** re-exported from web or mobile: follow Step 0 — ensure deprecation is visible on **every** public import path (common barrels **and** web/mobile re-exports if applicable). +- **Visualization** packages: same rules whenever those packages export the symbol. + +--- + +## Step 4 — Docsite metadata (`apps/docs/docs`) + +Only when the symbol has **existing** docs under the docs app. **Do not** add new doc folders unless the docs workflow already expects them. + +### Components (`apps/docs/docs/components/`) + +1. Locate the folder, e.g. `apps/docs/docs/components///`. +2. If present, edit **`webMetadata.json`** and/or **`mobileMetadata.json`** (some components have both; some only one). +3. Add or update the top-level **`warning`** string: + +```json +"warning": "This component is deprecated. Please use {replacement} instead." +``` + +Examples: + +- `"Please use Tabs instead."` +- `"Please use MediaCard instead."` + +Match the tone of existing deprecations when the replacement is not a single component name (e.g. “Use indeterminate ProgressCircle for loading indicators instead.”) — still keep the opening: **This component is deprecated.** + +### Hooks (`apps/docs/docs/hooks/`) + +1. Locate the hook folder, e.g. `apps/docs/docs/hooks//`. +2. Hooks may use **`webMetadata.json`** and **`mobileMetadata.json`**, or a single shared **`metadata.json`** — update whichever file(s) exist for that hook. +3. Add or update the top-level **`warning`** string using **hook** wording: + +```json +"warning": "This hook is deprecated. Please use {replacement} instead." +``` + +Use the same `{replacement}` phrasing as in JSDoc. If the replacement is not a single hook name, adapt the sentence but keep the opening: **This hook is deprecated.** + +--- + +## Step 5 — Verification checklist + +- [ ] Every **public export path** across packages that expose the symbol has been found (Step 0) and carries deprecation (implementation and re-exports as needed). +- [ ] `@deprecated` includes replacement guidance and the exact closing sentence about future major removal. +- [ ] **`@deprecationExpectedRemoval v`** matches the **confirmed** removal major (Step 2), not an unverified default. +- [ ] **Web + mobile** implementations and metadata (when applicable) are updated; nothing skipped because the symbol was “only” defined in common or another package. +- [ ] `warning` in metadata matches the replacement story: **this component is deprecated** for component docs, **this hook is deprecated** for hook docs (`apps/docs/docs/hooks/`). +- [ ] **`yarn nx run :lint`** has been run for every touched project (**Step 6**) and passes. + +--- + +## Step 6 — Run ESLint (required) + +After all edits, **run the `lint` target** on **every Nx project** that contains changed source files so **`internal/deprecated-jsdoc-has-removal-version`** (and the rest of the package lint config) passes. + +Use the workspace convention: + +```bash +yarn nx run :lint +``` + +Examples: `web`, `mobile`, `common`, `web-visualization`, `mobile-visualization` — run **each** project you touched. Fix any reported issues before finishing (most often: missing `@deprecationExpectedRemoval`, or `@deprecated` text not ending with the standard sentence). + +--- + +## Reference examples in-repo + +- JSDoc: search for `@deprecationExpectedRemoval` under `packages/web/src` (e.g. `TabNavigation.tsx`, `Spinner.tsx`). +- ESLint: `libs/eslint-plugin-internal` — rule **`deprecated-jsdoc-has-removal-version`** (exposed as **`internal/deprecated-jsdoc-has-removal-version`** in the root `eslint.config.mjs`). +- Metadata: search for `"warning": "This component is deprecated` under `apps/docs/docs/components`; hook docs live under `apps/docs/docs/hooks/` (use **This hook is deprecated** for hook `warning` text). diff --git a/packages/common/CHANGELOG.md b/packages/common/CHANGELOG.md index d5852f4788..7f08986c31 100644 --- a/packages/common/CHANGELOG.md +++ b/packages/common/CHANGELOG.md @@ -8,6 +8,12 @@ All notable changes to this project will be documented in this file. +## Unreleased + +#### 📘 Misc + +- Deprecate Card-related types. [[#562](https://github.com/coinbase/cds/pull/562)] + ## 8.60.0 (3/29/2026 PST) #### 🚀 Updates diff --git a/packages/common/src/types/CardHeaderProps.ts b/packages/common/src/types/CardHeaderProps.ts index 924564b48b..892be22370 100644 --- a/packages/common/src/types/CardHeaderProps.ts +++ b/packages/common/src/types/CardHeaderProps.ts @@ -1,5 +1,9 @@ import type { SharedProps } from './SharedProps'; +/** + * @deprecated Use ContentCardHeaderProps instead. This will be removed in a future major release. + * @deprecationExpectedRemoval v10 + */ export type CardHeaderProps = { /** Absolute or Relative path to Avatar */ avatar?: string; diff --git a/packages/common/src/types/CardMediaProps.ts b/packages/common/src/types/CardMediaProps.ts index aec5461eb1..2352881153 100644 --- a/packages/common/src/types/CardMediaProps.ts +++ b/packages/common/src/types/CardMediaProps.ts @@ -15,6 +15,10 @@ export type CardMediaImageSizeObject = aspectRatio: AspectRatio; }; +/** + * @deprecated Use SpotSquare when `type` is "spotSquare", Pictogram when `type` is "pictogram", or RemoteImage when `type` is "image". This will be removed in a future major release. + * @deprecationExpectedRemoval v10 + */ export type CardMediaProps = { /** Informs how to auto-magically size the media. */ placement: CardMediaPlacement; diff --git a/packages/mobile/CHANGELOG.md b/packages/mobile/CHANGELOG.md index dacd500bf1..cc21a04190 100644 --- a/packages/mobile/CHANGELOG.md +++ b/packages/mobile/CHANGELOG.md @@ -12,6 +12,10 @@ All notable changes to this project will be documented in this file. #### 📘 Misc +- Deprecate Card and its sub-components. [[#562](https://github.com/coinbase/cds/pull/562)] + +#### 📘 Misc + - Chore: deprecate CardGroup. [[#560](https://github.com/coinbase/cds/pull/560)] ## 8.60.0 (3/29/2026 PST) diff --git a/packages/mobile/src/cards/Card.tsx b/packages/mobile/src/cards/Card.tsx index 96678f0013..a3acd00297 100644 --- a/packages/mobile/src/cards/Card.tsx +++ b/packages/mobile/src/cards/Card.tsx @@ -58,6 +58,10 @@ const getBorderRadiusPinStyle = (borderRadius: number) => ({ all: {}, }); +/** + * @deprecated Use ContentCard instead. This will be removed in a future major release. + * @deprecationExpectedRemoval v10 + */ export const Card = memo(function OldCard({ children, background = 'bg', diff --git a/packages/mobile/src/cards/CardBody.tsx b/packages/mobile/src/cards/CardBody.tsx index 13748f95ca..c42de534c0 100644 --- a/packages/mobile/src/cards/CardBody.tsx +++ b/packages/mobile/src/cards/CardBody.tsx @@ -92,6 +92,10 @@ const CardBodyAction = memo(function CardBodyAction({ ); }); +/** + * @deprecated Use ContentCardBody instead. This will be removed in a future major release. + * @deprecationExpectedRemoval v10 + */ export const CardBody = memo(function CardBody({ testID = 'card-body', title, diff --git a/packages/mobile/src/cards/CardFooter.tsx b/packages/mobile/src/cards/CardFooter.tsx index c55ec5518c..d122674aaf 100644 --- a/packages/mobile/src/cards/CardFooter.tsx +++ b/packages/mobile/src/cards/CardFooter.tsx @@ -16,6 +16,10 @@ export type CardFooterBaseProps = Pick< export type CardFooterProps = CardFooterBaseProps & Omit; +/** + * @deprecated Use ContentCardFooter instead. This will be removed in a future major release. + * @deprecationExpectedRemoval v10 + */ export const CardFooter = memo(function CardFooter({ children, paddingBottom = 2, diff --git a/packages/mobile/src/cards/CardHeader.tsx b/packages/mobile/src/cards/CardHeader.tsx index 0c868ed308..e0259c5cdf 100644 --- a/packages/mobile/src/cards/CardHeader.tsx +++ b/packages/mobile/src/cards/CardHeader.tsx @@ -5,8 +5,16 @@ import { HStack } from '../layout/HStack'; import { RemoteImage } from '../media/RemoteImage'; import { Text } from '../typography/Text'; +/** + * @deprecated Use ContentCardHeaderProps instead. This will be removed in a future major release. + * @deprecationExpectedRemoval v10 + */ export type CardHeaderProps = CardHeaderBaseProps; +/** + * @deprecated Use ContentCardHeader instead. This will be removed in a future major release. + * @deprecationExpectedRemoval v10 + */ export const CardHeader = memo( ({ avatar, metaData, description, action, testID }: CardHeaderProps) => { return ( diff --git a/packages/mobile/src/cards/CardMedia.tsx b/packages/mobile/src/cards/CardMedia.tsx index f7bb277370..ab726cec0a 100644 --- a/packages/mobile/src/cards/CardMedia.tsx +++ b/packages/mobile/src/cards/CardMedia.tsx @@ -13,6 +13,10 @@ import type { import { Pictogram, SpotSquare } from '../illustrations'; import { getSource, RemoteImage } from '../media/RemoteImage'; +/** + * @deprecated Use SpotSquare when `type` is "spotSquare", Pictogram when `type` is "pictogram", or RemoteImage when `type` is "image". This will be removed in a future major release. + * @deprecationExpectedRemoval v10 + */ export type CardMediaProps = CommonCardMediaProps; const imageProps: Record = { @@ -27,6 +31,10 @@ const imageProps: Record = { end: defaultMediaSize, }; +/** + * @deprecated Use SpotSquare when `type` is "spotSquare", Pictogram when `type` is "pictogram", or RemoteImage when `type` is "image". This will be removed in a future major release. + * @deprecationExpectedRemoval v10 + */ export const CardMedia = memo(function CardMedia({ placement = 'end', ...props }: CardMediaProps) { switch (props.type) { case 'spotSquare': diff --git a/packages/web/CHANGELOG.md b/packages/web/CHANGELOG.md index ab67ca2fec..68f6d8bb71 100644 --- a/packages/web/CHANGELOG.md +++ b/packages/web/CHANGELOG.md @@ -12,6 +12,10 @@ All notable changes to this project will be documented in this file. #### 📘 Misc +- Deprecate Card and its sub-components. [[#562](https://github.com/coinbase/cds/pull/562)] + +#### 📘 Misc + - Chore: deprecate CardGroup. [[#560](https://github.com/coinbase/cds/pull/560)] ## 8.60.0 (3/29/2026 PST) diff --git a/packages/web/src/cards/Card.tsx b/packages/web/src/cards/Card.tsx index d26005bddc..43f676b497 100644 --- a/packages/web/src/cards/Card.tsx +++ b/packages/web/src/cards/Card.tsx @@ -20,6 +20,10 @@ export type CardBaseProps = Pick & export type CardProps = CardBaseProps & Omit, 'onClick' | 'onKeyDown' | 'onKeyUp' | 'background'>; +/** + * @deprecated Use ContentCard instead. This will be removed in a future major release. + * @deprecationExpectedRemoval v10 + */ export const Card = memo(function Card({ children, background = 'bg', diff --git a/packages/web/src/cards/CardBody.tsx b/packages/web/src/cards/CardBody.tsx index 56683db4fd..74b7702346 100644 --- a/packages/web/src/cards/CardBody.tsx +++ b/packages/web/src/cards/CardBody.tsx @@ -62,6 +62,9 @@ export type CardBodyProps = CardBodyBaseProps & Omit /** * Provides an opinionated layout for the typical content of a Card: a title, description, media, and action + * + * @deprecated Use ContentCardBody instead. This will be removed in a future major release. + * @deprecationExpectedRemoval v10 */ export const CardBody = memo(function CardBody({ testID = 'card-body', diff --git a/packages/web/src/cards/CardFooter.tsx b/packages/web/src/cards/CardFooter.tsx index 4740515388..5f5dfcb1d2 100644 --- a/packages/web/src/cards/CardFooter.tsx +++ b/packages/web/src/cards/CardFooter.tsx @@ -13,6 +13,10 @@ export type CardFooterBaseProps = Pick & export type CardFooterProps = CardFooterBaseProps & Omit, 'children'>; +/** + * @deprecated Use ContentCardFooter instead. This will be removed in a future major release. + * @deprecationExpectedRemoval v10 + */ export const CardFooter: React.FC> = memo( function CardFooter({ children, paddingBottom = 2, paddingX = gutter, testID, ...otherProps }) { return ( diff --git a/packages/web/src/cards/CardHeader.tsx b/packages/web/src/cards/CardHeader.tsx index b575bd0881..cc227015ce 100644 --- a/packages/web/src/cards/CardHeader.tsx +++ b/packages/web/src/cards/CardHeader.tsx @@ -7,6 +7,10 @@ import { VStack } from '../layout/VStack'; import { Avatar } from '../media/Avatar'; import { Text } from '../typography/Text'; +/** + * @deprecated Use ContentCardHeader instead. This will be removed in a future major release. + * @deprecationExpectedRemoval v10 + */ export const CardHeader = memo(function CardHeader({ avatar, metaData, diff --git a/packages/web/src/cards/CardMedia.tsx b/packages/web/src/cards/CardMedia.tsx index 42b70225e8..e661c491ed 100644 --- a/packages/web/src/cards/CardMedia.tsx +++ b/packages/web/src/cards/CardMedia.tsx @@ -26,6 +26,10 @@ const imageProps: Record = { end: defaultMediaSize, }; +/** + * @deprecated Use SpotSquare when `type` is "spotSquare", Pictogram when `type` is "pictogram", or RemoteImage when `type` is "image". This will be removed in a future major release. + * @deprecationExpectedRemoval v10 + */ export const CardMedia = memo(function CardMedia({ placement = 'end', ...props }: CardMediaProps) { if (props.type === 'spotSquare') { return (