diff --git a/apps/docs/docs/components/cards/MessagingCard/_mobileExamples.mdx b/apps/docs/docs/components/cards/MessagingCard/_mobileExamples.mdx index 70c7b2dece..544ffc5bf5 100644 --- a/apps/docs/docs/components/cards/MessagingCard/_mobileExamples.mdx +++ b/apps/docs/docs/components/cards/MessagingCard/_mobileExamples.mdx @@ -75,7 +75,12 @@ Use `mediaPlacement` to control the position of media content. ## Upsell Card Styles -MessagingCard with `type="upsell"` supports various background colors to match different promotional purposes. Use the `background` prop for semantic tokens, or `dangerouslySetBackground` for custom spectrum colors. +MessagingCard with `type="upsell"` supports various background colors to match different promotional purposes. Use the `background` prop for semantic tokens. + +For **custom background colors**, use the recommended approach: + +- **Non-interactive cards** (`renderAsPressable={false}` or omitted): set the background via `styles.root` (e.g. `styles={{ root: { backgroundColor: 'rgb(...)' } }}`). +- **Interactive cards** (`renderAsPressable` with `onPress`): set the background via `blendStyles.background` (e.g. `blendStyles={{ background: 'rgb(...)' }}`) so press states are handled correctly. ### General Upsell @@ -140,7 +145,7 @@ function FeatureUpsell() { Up to 3.29% APR on ETH @@ -182,7 +187,7 @@ function CommunityUpsell() { Join the community @@ -244,7 +249,7 @@ function ProductUpsell() { {card.title} @@ -291,7 +296,7 @@ function NewsUpsell() { Help defend crypto in America @@ -366,7 +371,7 @@ function DismissibleCards() { console.log('Card pressed!')} type="upsell" - dangerouslySetBackground={`rgb(${spectrum.teal70})`} + blendStyles={{ background: `rgb(${spectrum.teal70})` }} title="Interactive Upsell" description="Tap to interact" width={320} @@ -745,7 +752,7 @@ function MultipleCards() { renderAsPressable onPress={() => console.log('clicked')} type="upsell" - dangerouslySetBackground={`rgb(${spectrum.purple70})`} + blendStyles={{ background: `rgb(${spectrum.purple70})` }} title="Card 3" description="Card with onPress handler" tag="Action" @@ -820,7 +827,7 @@ When you need both `onDismissButtonPress` and want the entire card to be pressab ### Color Contrast -MessagingCard supports custom backgrounds via `background` and `dangerouslySetBackground` props. When using custom background colors, ensure sufficient color contrast between text and background: +MessagingCard supports custom backgrounds via the `background` prop and, for custom colors, `styles.root` (non-interactive) or `blendStyles.background` (interactive). When using custom background colors, ensure sufficient color contrast between text and background: - Use `fgInverse` text color with dark backgrounds (e.g., `accentBoldPurple`, `bgInverse`) - Use `fg` text color with light backgrounds (e.g., `bgPrimaryWash`, `bgAlternate`) diff --git a/apps/docs/docs/components/cards/MessagingCard/_webExamples.mdx b/apps/docs/docs/components/cards/MessagingCard/_webExamples.mdx index 7bb204e61e..a827cc7e72 100644 --- a/apps/docs/docs/components/cards/MessagingCard/_webExamples.mdx +++ b/apps/docs/docs/components/cards/MessagingCard/_webExamples.mdx @@ -75,7 +75,12 @@ Use `mediaPlacement` to control the position of media content. ## Upsell Card Styles -MessagingCard with `type="upsell"` supports various background colors to match different promotional purposes. Use the `background` prop for semantic tokens, or `dangerouslySetBackground` for custom spectrum colors. +MessagingCard with `type="upsell"` supports various background colors to match different promotional purposes. Use the `background` prop for semantic tokens. + +For **custom background colors**, use the recommended approach: + +- **Non-interactive cards** (default `as="article"` or `renderAsPressable={false}`): set the background via `styles.root` or `classNames.root` (e.g. `styles={{ root: { backgroundColor: 'rgb(var(--blue80))' } }}`). +- **Interactive cards** (`renderAsPressable` with `as="a"` or `as="button"`): set the background via `blendStyles.background` (e.g. `blendStyles={{ background: 'rgb(var(--blue80))' }}`) so press states are handled correctly. ### General Upsell @@ -130,7 +135,7 @@ function FeatureUpsell() { Up to 3.29% APR on ETH @@ -179,7 +184,7 @@ function CommunityUpsell() { Join the community @@ -240,7 +245,7 @@ function ProductUpsell() { {card.title} @@ -286,7 +291,7 @@ function NewsUpsell() { Help defend crypto in America @@ -373,7 +378,7 @@ Use `onDismissButtonClick` to add a dismiss button. mediaPlacement="end" onDismissButtonClick={() => alert('Card dismissed!')} dismissButtonAccessibilityLabel="Close card" - dangerouslySetBackground="rgb(var(--teal70))" + styles={{ root: { backgroundColor: 'rgb(var(--teal70))' } }} /> alert('Card clicked!')} type="upsell" - dangerouslySetBackground="rgb(var(--gray100))" + blendStyles={{ background: 'rgb(var(--gray100))' }} title="Interactive Card" description="Clickable card with onClick handler" width={320} @@ -771,7 +780,7 @@ Display multiple cards in a carousel. as="button" onClick={() => console.log('clicked')} type="upsell" - dangerouslySetBackground="rgb(var(--purple70))" + blendStyles={{ background: 'rgb(var(--purple70))' }} title="Card 3" description="Card with onClick handler" tag="Action" @@ -850,7 +859,7 @@ When you need both `onDismissButtonClick` and want the entire card to be clickab ### Color Contrast -MessagingCard supports custom backgrounds via `background` and `dangerouslySetBackground` props. When using custom background colors, ensure sufficient color contrast between text and background: +MessagingCard supports custom backgrounds via the `background` prop and, for custom colors, `styles.root` / `classNames.root` (non-interactive) or `blendStyles.background` (interactive). When using custom background colors, ensure sufficient color contrast between text and background: - Use `fgInverse` text color with dark backgrounds (e.g., `accentBoldPurple`, `bgInverse`) - Use `fg` text color with light backgrounds (e.g., `bgPrimaryWash`, `bgAlternate`) diff --git a/apps/docs/src/test-no-dangerously-set-background.tsx b/apps/docs/src/test-no-dangerously-set-background.tsx new file mode 100644 index 0000000000..3fe31770cc --- /dev/null +++ b/apps/docs/src/test-no-dangerously-set-background.tsx @@ -0,0 +1,24 @@ +/** + * Temporary file to test @coinbase/cds/no-dangerously-set-background in the docs app. + * Run: yarn eslint apps/docs/src/test-no-dangerously-set-background.tsx + * You should see a warning on the Button line. Delete this file when done. + */ +import { Button } from '@coinbase/cds-web/buttons'; +import { MessagingCard as MessagingCardComponent } from '@coinbase/cds-web/cards/MessagingCard'; +import { Interactable } from '@coinbase/cds-web/system/Interactable'; + +export function TestComponent() { + return ( + <> + This should trigger the rule + + This should trigger the rule + + + ); +} diff --git a/eslint.config.mjs b/eslint.config.mjs index 3962a700d4..440449a5a0 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -14,6 +14,7 @@ import eslintReactNativeA11y from 'eslint-plugin-react-native-a11y'; import eslintReactNative from 'eslint-plugin-react-native'; import eslintCodegen from 'eslint-plugin-codegen'; import internalPlugin from '@coinbase/eslint-plugin-internal'; +import cdsPlugin from '@coinbase/eslint-plugin-cds'; const ignores = [ '*.md', @@ -286,6 +287,16 @@ export default tseslint.config( ...packageProductionRules, }, }, + // CDS rule for docs app (no-dangerously-set-background) + { + files: ['apps/docs/**/*.{ts,tsx}'], + plugins: { + '@coinbase/cds': cdsPlugin, + }, + rules: { + '@coinbase/cds/no-dangerously-set-background': 'warn', + }, + }, { files: ['**/*mobile*/**/*.{ts,tsx}'], settings: sharedSettings, diff --git a/package.json b/package.json index c998c73387..382c40de09 100644 --- a/package.json +++ b/package.json @@ -110,6 +110,7 @@ "@babel/runtime": "^7.28.2", "@babel/template": "^7.20.7", "@babel/types": "^7.20.7", + "@coinbase/eslint-plugin-cds": "workspace:^", "@coinbase/eslint-plugin-internal": "workspace:^", "@figma/code-connect": "^1.3.13", "@graphql-tools/jest-transform": "^2.0.0", diff --git a/packages/common/CHANGELOG.md b/packages/common/CHANGELOG.md index d147ed5778..ccade5c2fd 100644 --- a/packages/common/CHANGELOG.md +++ b/packages/common/CHANGELOG.md @@ -8,6 +8,10 @@ All notable changes to this project will be documented in this file. +## 8.47.2 ((2/19/2026, 03:18 PM PST)) + +This is an artificial version bump with no new change. + ## 8.47.1 ((2/19/2026, 01:18 PM PST)) This is an artificial version bump with no new change. diff --git a/packages/common/package.json b/packages/common/package.json index 59ae5d9958..9a016cc3c6 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -1,6 +1,6 @@ { "name": "@coinbase/cds-common", - "version": "8.47.1", + "version": "8.47.2", "description": "Coinbase Design System - Common", "repository": { "type": "git", diff --git a/packages/eslint-plugin-cds/CHANGELOG.md b/packages/eslint-plugin-cds/CHANGELOG.md index cf9ea664ff..d0d90f5694 100644 --- a/packages/eslint-plugin-cds/CHANGELOG.md +++ b/packages/eslint-plugin-cds/CHANGELOG.md @@ -8,6 +8,12 @@ All notable changes to this project will be documented in this file. +## 3.3.0 (2/19/2026 PST) + +#### 🚀 Updates + +- Add lint rule to avoid dangerouslySetBackground on Interactable. [[#405](https://github.com/coinbase/cds/pull/405)] + ## 3.2.1 (10/1/2025 PST) #### 🐞 Fixes diff --git a/packages/eslint-plugin-cds/README.md b/packages/eslint-plugin-cds/README.md index 1bc4cdf534..98046cb408 100644 --- a/packages/eslint-plugin-cds/README.md +++ b/packages/eslint-plugin-cds/README.md @@ -199,7 +199,15 @@ This rule also checks for other required a11y labels that need to be enforced ou ### Current CDS Best Practices Rules -TBD +#### no-dangerously-set-background (Web & Mobile) + +**Rule Description**: + +The `no-dangerously-set-background` rule warns when the `dangerouslySetBackground` prop is used on **Interactable**, **Pressable**, or **Card components** (MessagingCard, MediaCard, DataCard) when the card is interactive. For those card components, the rule only runs when `renderAsPressable` is explicitly set to `true` (or the shorthand `renderAsPressable`). Cards without `renderAsPressable` or with `renderAsPressable={false}` are ignored. Other components (e.g. Box, UpsellCard, CardRoot) are ignored. + +**Why**: Background color applied via `dangerouslySetBackground` is not picked up by the color blending logic. As a result, the interactable does not display the correct background in its hovered, pressed, and disabled states. Use `blendStyles.background` so the blending logic applies and these states render correctly. + +The rule is enabled by default in both `configs.web` and `configs.mobile` (and legacy configs) as `warn`. ## Development diff --git a/packages/eslint-plugin-cds/package.json b/packages/eslint-plugin-cds/package.json index 3d3e190e8d..f5cfdd078c 100644 --- a/packages/eslint-plugin-cds/package.json +++ b/packages/eslint-plugin-cds/package.json @@ -1,6 +1,6 @@ { "name": "@coinbase/eslint-plugin-cds", - "version": "3.2.1", + "version": "3.3.0", "description": "ESLint plugin for CDS", "repository": { "type": "git", diff --git a/packages/eslint-plugin-cds/src/configs/mobile.ts b/packages/eslint-plugin-cds/src/configs/mobile.ts index e09ef3e05c..5f9c98ccdb 100644 --- a/packages/eslint-plugin-cds/src/configs/mobile.ts +++ b/packages/eslint-plugin-cds/src/configs/mobile.ts @@ -23,6 +23,7 @@ export function buildMobileConfig(plugin: Record) { rules: { 'react-native-a11y/has-accessibility-hint': 'off', '@coinbase/cds/has-valid-accessibility-descriptors-extended': 'warn', + '@coinbase/cds/no-dangerously-set-background': 'warn', }, }; } @@ -32,5 +33,6 @@ export const legacyMobileConfig = { rules: { 'react-native-a11y/has-accessibility-hint': 'off', '@coinbase/cds/has-valid-accessibility-descriptors-extended': 'warn', + '@coinbase/cds/no-dangerously-set-background': 'warn', }, }; diff --git a/packages/eslint-plugin-cds/src/configs/web.ts b/packages/eslint-plugin-cds/src/configs/web.ts index 411facf7b6..1ef69d855e 100644 --- a/packages/eslint-plugin-cds/src/configs/web.ts +++ b/packages/eslint-plugin-cds/src/configs/web.ts @@ -22,6 +22,7 @@ export function buildWebConfig(plugin: Record) { }, rules: { '@coinbase/cds/control-has-associated-label-extended': 'warn', + '@coinbase/cds/no-dangerously-set-background': 'warn', '@coinbase/cds/no-v7-imports': 'warn', 'jsx-a11y/control-has-associated-label': [ 'warn', @@ -48,6 +49,7 @@ export const legacyWebConfig = { plugins: ['jsx-a11y'], rules: { '@coinbase/cds/control-has-associated-label-extended': 'warn', + '@coinbase/cds/no-dangerously-set-background': 'warn', '@coinbase/cds/no-v7-imports': 'warn', }, overrides: [ diff --git a/packages/eslint-plugin-cds/src/rules.ts b/packages/eslint-plugin-cds/src/rules.ts index 68cc384151..26eb8c10cf 100644 --- a/packages/eslint-plugin-cds/src/rules.ts +++ b/packages/eslint-plugin-cds/src/rules.ts @@ -2,11 +2,13 @@ import type { TSESLint } from '@typescript-eslint/utils'; import { controlHasAssociatedLabelExtended } from './rules/control-has-associated-label-extended'; import { hasValidA11yDescriptorsExtended } from './rules/has-valid-accessibility-descriptors-extended'; +import { noDangerouslySetBackground } from './rules/no-dangerously-set-background'; import { noV7Imports } from './rules/no-v7-imports'; export const rules = { 'control-has-associated-label-extended': controlHasAssociatedLabelExtended, 'has-valid-accessibility-descriptors-extended': hasValidA11yDescriptorsExtended, + 'no-dangerously-set-background': noDangerouslySetBackground, 'no-v7-imports': noV7Imports, } as const satisfies { [key: string]: TSESLint.RuleModule; diff --git a/packages/eslint-plugin-cds/src/rules/no-dangerously-set-background.ts b/packages/eslint-plugin-cds/src/rules/no-dangerously-set-background.ts new file mode 100644 index 0000000000..f39e7692f3 --- /dev/null +++ b/packages/eslint-plugin-cds/src/rules/no-dangerously-set-background.ts @@ -0,0 +1,156 @@ +import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + +export const RULE_NAME = 'no-dangerously-set-background'; + +type MessageIds = 'usePreferredApi'; + +const CDS_PACKAGE_PREFIXES = ['@coinbase/cds-web', '@coinbase/cds-mobile']; + +const INTERACTABLE_PRESSABLE_OR_BUTTON = new Set(['Interactable', 'Pressable', 'Button']); +const CARD_COMPONENTS = new Set(['MessagingCard', 'MediaCard', 'DataCard']); + +function getComponentName(nameNode: TSESTree.JSXTagNameExpression): string | null { + if (nameNode.type === 'JSXIdentifier') { + return nameNode.name; + } + if (nameNode.type === 'JSXMemberExpression') { + const property = nameNode.property.type === 'JSXIdentifier' ? nameNode.property.name : null; + return property ?? null; + } + return null; +} + +function getImportLocalName(nameNode: TSESTree.JSXTagNameExpression): string | null { + if (nameNode.type === 'JSXIdentifier') { + return nameNode.name; + } + if (nameNode.type === 'JSXMemberExpression') { + return nameNode.object.type === 'JSXIdentifier' ? nameNode.object.name : null; + } + return null; +} + +function isCdsImportSource(source: string): boolean { + return CDS_PACKAGE_PREFIXES.some( + (prefix) => source === prefix || source.startsWith(`${prefix}/`), + ); +} + +function hasRenderAsPressableTrue(attributes: TSESTree.JSXAttribute[]): boolean { + const attr = attributes.find( + (a) => + a.type === 'JSXAttribute' && + a.name.type === 'JSXIdentifier' && + a.name.name === 'renderAsPressable', + ); + if (!attr) { + return false; + } + if (!attr.value) { + return true; + } + if (attr.value.type !== 'JSXExpressionContainer') { + return false; + } + const expr = attr.value.expression; + return expr.type === 'Literal' && expr.value === true; +} + +/** + * Warns when dangerouslySetBackground is used on Interactable, Pressable, Button, or + * Card components (MessagingCard, MediaCard, DataCard with renderAsPressable true). Use blendStyles.background + * so the interactable displays the correct color in hovered, pressed, and + * disabled states. + */ +export const noDangerouslySetBackground: TSESLint.RuleModule = { + meta: { + type: 'suggestion', + docs: { + description: + 'Disallow dangerouslySetBackground on Interactable, Pressable, Button, and Card components (MessagingCard, MediaCard, DataCard when renderAsPressable is true). Use blendStyles.background so the interactable displays the correct color in hovered, pressed, and disabled states.', + }, + messages: { + usePreferredApi: + 'Use blendStyles.background instead so the interactable displays the correct color in hovered, pressed, and disabled states.', + }, + schema: [], + }, + defaultOptions: [], + create(context: TSESLint.RuleContext) { + const importedFromCds: Record = {}; + const canonicalNameByLocalName: Record = {}; + + return { + ImportDeclaration(node: TSESTree.ImportDeclaration) { + const source = typeof node.source.value === 'string' ? node.source.value : null; + if (source === null || !isCdsImportSource(source)) { + return; + } + for (const specifier of node.specifiers) { + if ( + specifier.type === 'ImportSpecifier' || + specifier.type === 'ImportDefaultSpecifier' || + specifier.type === 'ImportNamespaceSpecifier' + ) { + const localName = specifier.local.name; + importedFromCds[localName] = source; + canonicalNameByLocalName[localName] = + specifier.type === 'ImportSpecifier' && specifier.imported.type === 'Identifier' + ? specifier.imported.name + : localName; + } + } + }, + JSXOpeningElement(node: TSESTree.JSXOpeningElement) { + const dangerouslySetBackgroundAttr = node.attributes.find( + (attr): attr is TSESTree.JSXAttribute => + attr.type === 'JSXAttribute' && + attr.name.type === 'JSXIdentifier' && + attr.name.name === 'dangerouslySetBackground', + ); + + if (!dangerouslySetBackgroundAttr) { + return; + } + + const importLocalName = getImportLocalName(node.name); + if (importLocalName === null) { + return; + } + + const source = importedFromCds[importLocalName]; + if (!source || !isCdsImportSource(source)) { + return; + } + + const canonicalName = canonicalNameByLocalName[importLocalName] ?? importLocalName; + const nameForSetCheck = + node.name.type === 'JSXMemberExpression' ? getComponentName(node.name) : canonicalName; + if (nameForSetCheck === null) { + return; + } + + if (INTERACTABLE_PRESSABLE_OR_BUTTON.has(nameForSetCheck)) { + context.report({ + node: dangerouslySetBackgroundAttr, + messageId: 'usePreferredApi', + }); + return; + } + + if (CARD_COMPONENTS.has(nameForSetCheck)) { + const jsxAttrs = node.attributes.filter( + (a): a is TSESTree.JSXAttribute => a.type === 'JSXAttribute', + ); + if (!hasRenderAsPressableTrue(jsxAttrs)) { + return; + } + context.report({ + node: dangerouslySetBackgroundAttr, + messageId: 'usePreferredApi', + }); + } + }, + }; + }, +}; diff --git a/packages/eslint-plugin-cds/tests/no-dangerously-set-background.test.ts b/packages/eslint-plugin-cds/tests/no-dangerously-set-background.test.ts new file mode 100644 index 0000000000..3385bde04b --- /dev/null +++ b/packages/eslint-plugin-cds/tests/no-dangerously-set-background.test.ts @@ -0,0 +1,97 @@ +import { RuleTester } from '@typescript-eslint/rule-tester'; + +import { noDangerouslySetBackground } from '../src/rules/no-dangerously-set-background'; + +const ruleTester = new RuleTester({ + languageOptions: { + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }, +}); + +ruleTester.run( + 'no-dangerously-set-background', + noDangerouslySetBackground as unknown as Parameters[1], + { + valid: [ + { + code: '', + }, + { + code: '', + }, + { + code: '', + }, + { + code: '', + }, + { + code: '', + }, + { + code: '', + }, + { + code: '', + }, + { + code: '', + }, + { + code: '...', + }, + { + code: '', + }, + { + code: 'import { Button } from \'./my-button\'; const x = ;', + }, + ], + invalid: [ + { + code: 'import { Button as SubmitButton } from \'@coinbase/cds-web\'; const x = Submit;', + errors: [{ messageId: 'usePreferredApi' }], + }, + { + code: 'import { Interactable } from \'@coinbase/cds-web\'; const x = ;', + errors: [{ messageId: 'usePreferredApi' }], + }, + { + code: 'import { Pressable } from \'@coinbase/cds-web\'; const x = ;', + errors: [{ messageId: 'usePreferredApi' }], + }, + { + code: 'import { Button } from \'@coinbase/cds-web\'; const x = ;', + errors: [{ messageId: 'usePreferredApi' }], + }, + { + code: 'import { MessagingCard } from \'@coinbase/cds-web\'; const x = ;', + errors: [{ messageId: 'usePreferredApi' }], + }, + { + code: 'import { MessagingCard } from \'@coinbase/cds-web\'; const x = ;', + errors: [{ messageId: 'usePreferredApi' }], + }, + { + code: 'import { MediaCard } from \'@coinbase/cds-web\'; const x = ;', + errors: [{ messageId: 'usePreferredApi' }], + }, + { + code: 'import { DataCard } from \'@coinbase/cds-web\'; const x = ;', + errors: [{ messageId: 'usePreferredApi' }], + }, + { + code: 'import { Button } from \'@coinbase/cds-mobile\'; const x = ;', + errors: [{ messageId: 'usePreferredApi' }], + }, + { + code: 'import { MessagingCard } from \'@coinbase/cds-web/cards\'; const x = ;', + errors: [{ messageId: 'usePreferredApi' }], + }, + ], + }, +); diff --git a/packages/mcp-server/CHANGELOG.md b/packages/mcp-server/CHANGELOG.md index 5c16ee14fc..7295b17606 100644 --- a/packages/mcp-server/CHANGELOG.md +++ b/packages/mcp-server/CHANGELOG.md @@ -8,6 +8,10 @@ All notable changes to this project will be documented in this file. +## 8.47.2 ((2/19/2026, 03:18 PM PST)) + +This is an artificial version bump with no new change. + ## 8.47.1 ((2/19/2026, 01:18 PM PST)) This is an artificial version bump with no new change. diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index 4d43050992..5ea99e2fce 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -1,6 +1,6 @@ { "name": "@coinbase/cds-mcp-server", - "version": "8.47.1", + "version": "8.47.2", "description": "Coinbase Design System - MCP Server", "repository": { "type": "git", diff --git a/packages/mobile/CHANGELOG.md b/packages/mobile/CHANGELOG.md index 68d4a6b00b..d6b320056b 100644 --- a/packages/mobile/CHANGELOG.md +++ b/packages/mobile/CHANGELOG.md @@ -8,6 +8,12 @@ All notable changes to this project will be documented in this file. +## 8.47.2 (2/19/2026 PST) + +#### 🐞 Fixes + +- Fix mobile CardRoot style forwarding logic. [[#405](https://github.com/coinbase/cds/pull/405)] + ## 8.47.1 (2/19/2026 PST) #### 🐞 Fixes diff --git a/packages/mobile/package.json b/packages/mobile/package.json index a32e5ae86c..7f4cec0b36 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@coinbase/cds-mobile", - "version": "8.47.1", + "version": "8.47.2", "description": "Coinbase Design System - Mobile", "repository": { "type": "git", diff --git a/packages/mobile/src/cards/CardRoot.tsx b/packages/mobile/src/cards/CardRoot.tsx index 104d7eb4a1..1009dac5e6 100644 --- a/packages/mobile/src/cards/CardRoot.tsx +++ b/packages/mobile/src/cards/CardRoot.tsx @@ -27,7 +27,7 @@ export type CardRootProps = CardRootBaseProps & * When `renderAsPressable` is true, it renders as a Pressable component. */ export const CardRoot = memo( - forwardRef(({ children, style, renderAsPressable, ...props }, ref) => { + forwardRef(({ children, renderAsPressable, ...props }, ref) => { const Component = renderAsPressable ? Pressable : HStack; return ( diff --git a/packages/mobile/src/cards/__stories__/MessagingCard.stories.tsx b/packages/mobile/src/cards/__stories__/MessagingCard.stories.tsx index 2e7b647f05..be84c8fed7 100644 --- a/packages/mobile/src/cards/__stories__/MessagingCard.stories.tsx +++ b/packages/mobile/src/cards/__stories__/MessagingCard.stories.tsx @@ -78,7 +78,7 @@ const DismissibleCardsExample = () => { height={100} resizeMode="cover" shape="rectangle" - source={{ uri: coinbaseOneLogo }} + source={coinbaseOneLogo} width={80} /> ) : ( @@ -125,7 +125,7 @@ const MessagingCardScreen = () => { height={120} resizeMode="cover" shape="rectangle" - source={{ uri: coinbaseOneLogo }} + source={coinbaseOneLogo} width={90} /> } @@ -196,7 +196,7 @@ const MessagingCardScreen = () => { height={120} resizeMode="cover" shape="rectangle" - source={{ uri: coinbaseOneLogo }} + source={coinbaseOneLogo} width={90} /> } @@ -224,7 +224,7 @@ const MessagingCardScreen = () => { height={120} resizeMode="cover" shape="rectangle" - source={{ uri: coinbaseOneLogo }} + source={coinbaseOneLogo} width={90} /> } @@ -252,7 +252,7 @@ const MessagingCardScreen = () => { height={156} resizeMode="cover" shape="rectangle" - source={{ uri: coinbaseOneLogo }} + source={coinbaseOneLogo} width={120} /> } @@ -282,7 +282,7 @@ const MessagingCardScreen = () => { height={186} resizeMode="cover" shape="rectangle" - source={{ uri: coinbaseOneLogo }} + source={coinbaseOneLogo} width={130} /> } @@ -324,7 +324,7 @@ const MessagingCardScreen = () => { height={156} resizeMode="cover" shape="rectangle" - source={{ uri: coinbaseOneLogo }} + source={coinbaseOneLogo} width={120} /> } @@ -373,7 +373,7 @@ const MessagingCardScreen = () => { height={120} resizeMode="cover" shape="rectangle" - source={{ uri: coinbaseOneLogo }} + source={coinbaseOneLogo} width={90} /> } @@ -416,7 +416,7 @@ const MessagingCardScreen = () => { height={120} resizeMode="cover" shape="rectangle" - source={{ uri: coinbaseOneLogo }} + source={coinbaseOneLogo} width={90} /> } @@ -438,6 +438,76 @@ const MessagingCardScreen = () => { + {/* Custom Background Color */} + + + + } + mediaPlacement="end" + onPress={NoopFn} + title="Pressable with Custom Background" + type="upsell" + /> + } + mediaPlacement="end" + onPress={NoopFn} + title="Nudge with Custom Background" + type="nudge" + /> + + } + mediaPlacement="end" + renderAsPressable={false} + styles={{ root: { backgroundColor: '#1E5A9E' } }} + title="Non-pressable with Custom Background" + type="upsell" + /> + } + mediaPlacement="end" + renderAsPressable={false} + styles={{ root: { backgroundColor: '#FFF8E6' } }} + title="Non-pressable Nudge with Custom Background" + type="nudge" + /> + + + {/* Text Content */} @@ -450,7 +520,7 @@ const MessagingCardScreen = () => { height={160} resizeMode="cover" shape="rectangle" - source={{ uri: coinbaseOneLogo }} + source={coinbaseOneLogo} width={120} /> } @@ -471,7 +541,7 @@ const MessagingCardScreen = () => { height={140} resizeMode="cover" shape="rectangle" - source={{ uri: coinbaseOneLogo }} + source={coinbaseOneLogo} width={100} /> } diff --git a/packages/web/CHANGELOG.md b/packages/web/CHANGELOG.md index af54c22cbb..ed7b6ad17d 100644 --- a/packages/web/CHANGELOG.md +++ b/packages/web/CHANGELOG.md @@ -8,6 +8,10 @@ All notable changes to this project will be documented in this file. +## 8.47.2 ((2/19/2026, 03:18 PM PST)) + +This is an artificial version bump with no new change. + ## 8.47.1 ((2/19/2026, 01:18 PM PST)) This is an artificial version bump with no new change. diff --git a/packages/web/package.json b/packages/web/package.json index 60916e9577..4961adf451 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,6 +1,6 @@ { "name": "@coinbase/cds-web", - "version": "8.47.1", + "version": "8.47.2", "description": "Coinbase Design System - Web", "repository": { "type": "git", diff --git a/packages/web/src/cards/__stories__/MessagingCard.stories.tsx b/packages/web/src/cards/__stories__/MessagingCard.stories.tsx index 8699227929..95b7bd02b0 100644 --- a/packages/web/src/cards/__stories__/MessagingCard.stories.tsx +++ b/packages/web/src/cards/__stories__/MessagingCard.stories.tsx @@ -397,6 +397,82 @@ export const PolymorphicAndInteractive = (): JSX.Element => { ); }; +// Custom Background Color (use styles.root for non-interactive, blendStyles.background for interactive) +export const CustomBackgroundColor = (): JSX.Element => { + return ( + + + } + mediaPlacement="end" + onClick={() => alert('Card clicked!')} + title="Pressable with Custom Background" + type="upsell" + width={320} + /> + } + mediaPlacement="end" + target="_blank" + title="Link with Custom Background" + type="nudge" + width={320} + /> + + } + mediaPlacement="end" + renderAsPressable={false} + styles={{ root: { backgroundColor: 'rgb(var(--blue80))' } }} + title="Non-pressable with Custom Background" + type="upsell" + width={320} + /> + } + mediaPlacement="end" + renderAsPressable={false} + styles={{ root: { backgroundColor: 'rgb(var(--yellow20))' } }} + title="Non-pressable Nudge with Custom Background" + type="nudge" + width={320} + /> + + ); +}; + // Text Content export const TextContent = (): JSX.Element => { return ( diff --git a/yarn.lock b/yarn.lock index 90d2583137..f6f4e4fc81 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2708,7 +2708,7 @@ __metadata: languageName: unknown linkType: soft -"@coinbase/eslint-plugin-cds@workspace:packages/eslint-plugin-cds": +"@coinbase/eslint-plugin-cds@workspace:^, @coinbase/eslint-plugin-cds@workspace:packages/eslint-plugin-cds": version: 0.0.0-use.local resolution: "@coinbase/eslint-plugin-cds@workspace:packages/eslint-plugin-cds" dependencies: @@ -17655,6 +17655,7 @@ __metadata: "@babel/runtime": "npm:^7.28.2" "@babel/template": "npm:^7.20.7" "@babel/types": "npm:^7.20.7" + "@coinbase/eslint-plugin-cds": "workspace:^" "@coinbase/eslint-plugin-internal": "workspace:^" "@figma/code-connect": "npm:^1.3.13" "@graphql-tools/jest-transform": "npm:^2.0.0"