diff --git a/packages/common/CHANGELOG.md b/packages/common/CHANGELOG.md index f170fd7c60..3b507c3a7d 100644 --- a/packages/common/CHANGELOG.md +++ b/packages/common/CHANGELOG.md @@ -1,3 +1,7 @@ +## 9.29.0 (2026-09-23) + +This is an artificial version bump with no new change. + ## 9.28.0 (2026-09-18) ### 🚀 Features @@ -61,6 +65,10 @@ This is an artificial version bump with no new change. This is an artificial version bump with no new change. +## 9.23.0 ((8/26/2026, 09:18 AM PST)) + +This is an artificial version bump with no new change. + ## 9.22.0 ((8/25/2026, 08:11 AM PST)) This is an artificial version bump with no new change. diff --git a/packages/common/package.json b/packages/common/package.json index 361fc9dd60..8981dcd3ef 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -1,6 +1,6 @@ { "name": "@coinbase/cds-common", - "version": "9.28.0", + "version": "9.29.0", "description": "Coinbase Design System - Common", "repository": { "type": "git", diff --git a/packages/mcp-server/CHANGELOG.md b/packages/mcp-server/CHANGELOG.md index 74b0cccd59..3a8f702efa 100644 --- a/packages/mcp-server/CHANGELOG.md +++ b/packages/mcp-server/CHANGELOG.md @@ -1,3 +1,7 @@ +## 9.29.0 (2026-09-23) + +This is an artificial version bump with no new change. + ## 9.28.0 (2026-09-18) This is an artificial version bump with no new change. @@ -51,6 +55,10 @@ This is an artificial version bump with no new change. This is an artificial version bump with no new change. +## 9.23.0 ((8/26/2026, 09:18 AM PST)) + +This is an artificial version bump with no new change. + ## 9.22.0 ((8/25/2026, 08:11 AM 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 fa9d82987a..ef10711dfa 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -1,6 +1,6 @@ { "name": "@coinbase/cds-mcp-server", - "version": "9.28.0", + "version": "9.29.0", "description": "Coinbase Design System - MCP Server", "repository": { "type": "git", diff --git a/packages/mobile/CHANGELOG.md b/packages/mobile/CHANGELOG.md index f3f0170f11..4f652601b3 100644 --- a/packages/mobile/CHANGELOG.md +++ b/packages/mobile/CHANGELOG.md @@ -1,3 +1,9 @@ +## 9.29.0 (2026-09-23) + +### 🚀 Features + +- Let TextIcon respect IconGlyphSourceContext overrides. [[#862](https://github.com/coinbase/cds/pull/862)] + ## 9.28.0 (2026-09-18) ### 🚀 Features @@ -65,6 +71,12 @@ - Support font props on Tabs. [[#825](https://github.com/coinbase/cds/pull/825)] +## 9.23.0 (8/26/2026 PST) + +#### 🚀 Updates + +- Let TextIcon respect IconGlyphSourceContext overrides. [[#862](https://github.com/coinbase/cds/pull/862)] + ## 9.22.0 (8/25/2026 PST) #### 🚀 Updates diff --git a/packages/mobile/package.json b/packages/mobile/package.json index 18a4a076f9..57bf3bea0b 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@coinbase/cds-mobile", - "version": "9.28.0", + "version": "9.29.0", "description": "Coinbase Design System - Mobile", "repository": { "type": "git", diff --git a/packages/mobile/src/icons/TextIcon.tsx b/packages/mobile/src/icons/TextIcon.tsx index 107b671db3..f3b3b03aee 100644 --- a/packages/mobile/src/icons/TextIcon.tsx +++ b/packages/mobile/src/icons/TextIcon.tsx @@ -8,7 +8,7 @@ import { isDevelopment } from '@coinbase/cds-utils'; import { useTheme } from '../hooks/useTheme'; import type { IconProps } from './Icon'; -import { getIconSourceSize } from './Icon'; +import { DEFAULT_ICON_FONT_FAMILY, getIconSourceSize, useResolvedGlyph } from './createIcon'; export type TextIconProps = Pick & { name: IconName; @@ -23,6 +23,10 @@ export type TextIconProps = Pick & { style?: StyleProp; } ); + +/** Stable bound source so TextIcon participates in the same context resolution as Icon. */ +const cdsGlyphSource = { glyphMap, fontFamily: DEFAULT_ICON_FONT_FAMILY }; + /** * * This is a simplified, text-only version of the Icon component. @@ -41,36 +45,39 @@ export const TextIcon = memo(function TextIcon({ const theme = useTheme(); const Component = animated ? Animated.Text : Text; const iconSize = theme.iconSize[size]; - const sourceSize = getIconSourceSize(iconSize); const iconColor = theme.color[color]; + const resolved = useResolvedGlyph(cdsGlyphSource, { + name, + size, + pixelSize: iconSize, + active: Boolean(active), + }); + const styles = useMemo( () => [ { - fontFamily: 'CoinbaseIcons', + fontFamily: resolved?.fontFamily, fontSize: iconSize, color: iconColor, }, style, // TODO https://linear.app/coinbase/issue/CDS-1518/audit-potentially-harmful-reactnative-animated-pattern ] as StyleProp, - [style, iconColor, iconSize], + [style, iconColor, iconSize, resolved?.fontFamily], ); - const iconName = `${name}-${sourceSize}-${active ? 'active' : 'inactive'}`; - const glyph = glyphMap[iconName as keyof typeof glyphMap]; - - if (glyph === undefined) { + if (resolved === undefined) { if (isDevelopment()) { - console.error(`Unable to find glyph for icon name "${name}" with glyph key "${iconName}"`); + console.error(`Unable to find glyph for icon name "${name}" at size "${size}"`); } return null; } return ( - {glyph} + {resolved.char} ); }); diff --git a/packages/mobile/src/icons/__tests__/TextIcon.test.tsx b/packages/mobile/src/icons/__tests__/TextIcon.test.tsx new file mode 100644 index 0000000000..c1d88afcec --- /dev/null +++ b/packages/mobile/src/icons/__tests__/TextIcon.test.tsx @@ -0,0 +1,97 @@ +import type { IconName } from '@coinbase/cds-common/types/IconName'; +import { render, screen } from '@testing-library/react-native'; + +import { DefaultThemeProvider } from '../../utils/testHelpers'; +import { DEFAULT_ICON_FONT_FAMILY, type GlyphMap, IconGlyphSourceProvider } from '../createIcon'; +import { TextIcon } from '../TextIcon'; + +const INACTIVE_GLYPH = '\u2606'; // ☆ +const ACTIVE_GLYPH = '\u2605'; // ★ +const OTHER_GLYPH = '\u25B2'; // ▲ + +type DemoIconName = 'star'; + +const demoGlyphMap: GlyphMap = { + 'star-12-active': ACTIVE_GLYPH, + 'star-12-inactive': INACTIVE_GLYPH, + 'star-16-active': ACTIVE_GLYPH, + 'star-16-inactive': INACTIVE_GLYPH, + 'star-24-active': ACTIVE_GLYPH, + 'star-24-inactive': INACTIVE_GLYPH, +}; + +// TextIcon reads directly from the CDS glyphMap module; mock it so tests are +// independent of the published icon set. +jest.mock('@coinbase/cds-icons/glyphMap', () => ({ + glyphMap: { + 'star-12-active': '\u2605', + 'star-12-inactive': '\u2606', + 'star-16-active': '\u2605', + 'star-16-inactive': '\u2606', + 'star-24-active': '\u2605', + 'star-24-inactive': '\u2606', + }, +})); + +const renderTextIcon = (ui: React.ReactElement) => + render({ui}); + +describe('TextIcon', () => { + it('renders the CDS glyph by default', () => { + renderTextIcon(); + expect(screen.getByText(INACTIVE_GLYPH)).toBeTruthy(); + }); + + it('uses the default CDS font family', () => { + renderTextIcon(); + expect(screen.getByText(INACTIVE_GLYPH)).toHaveStyle({ fontFamily: DEFAULT_ICON_FONT_FAMILY }); + }); + + it('uses the glyph and font family from the context source when the name matches', () => { + renderTextIcon( + + + , + ); + + expect(screen.getByText(OTHER_GLYPH)).toHaveStyle({ fontFamily: 'RetailIcons' }); + expect(screen.queryByText(INACTIVE_GLYPH)).toBeNull(); + }); + + it('falls back to the CDS glyphMap when the context source does not cover the name', () => { + renderTextIcon( + + + , + ); + + expect(screen.getByText(INACTIVE_GLYPH)).toHaveStyle({ + fontFamily: DEFAULT_ICON_FONT_FAMILY, + }); + }); + + it('uses a custom getGlyph resolver from the context source', () => { + const getGlyph = jest.fn(() => OTHER_GLYPH); + renderTextIcon( + + + , + ); + + expect(screen.getByText(OTHER_GLYPH)).toHaveStyle({ fontFamily: 'CustomFont' }); + expect(getGlyph).toHaveBeenCalledWith(expect.objectContaining({ name: 'star', active: false })); + }); + + it('returns null when no glyph is found in either the context source or CDS glyphMap', () => { + const consoleError = jest.spyOn(console, 'error').mockImplementation(() => undefined); + renderTextIcon(); + expect(screen.queryByRole('image')).toBeNull(); + consoleError.mockRestore(); + }); +}); diff --git a/packages/mobile/src/icons/createIcon.tsx b/packages/mobile/src/icons/createIcon.tsx index 7655c9caa3..6cfaa3253e 100644 --- a/packages/mobile/src/icons/createIcon.tsx +++ b/packages/mobile/src/icons/createIcon.tsx @@ -52,7 +52,7 @@ export type IconGlyphSource = { getGlyph?: (args: IconGlyphResolverArgs) => string | undefined; }; -const IconGlyphSourceContext = createContext | undefined>(undefined); +export const IconGlyphSourceContext = createContext | undefined>(undefined); export type IconGlyphSourceProviderProps = { /** @@ -161,6 +161,19 @@ const resolveGlyph = ( return fromContext ?? resolveFromSource(boundSource, args); }; +/** + * Resolves a glyph for `name` against the nearest `IconGlyphSourceProvider`, + * falling back to `boundSource` when the context has no match. + * Extracts the shared lookup logic so both `createIcon` and `TextIcon` use it. + */ +export function useResolvedGlyph( + boundSource: IconGlyphSource, + args: Omit, 'glyphMap'>, +): ResolvedGlyph | undefined { + const contextSource = useContext(IconGlyphSourceContext); + return resolveGlyph(contextSource, boundSource, args); +} + /** Creates a typed `Icon` component bound to an icon set. */ export function createIcon(source: IconGlyphSource) { const Icon = memo(({ ref, ..._props }: IconProps & { ref?: React.Ref }) => { @@ -197,8 +210,7 @@ export function createIcon(source: IconGlyphSource) { const finalColor = dangerouslySetColor ?? iconColor; // Tried before the bound set, so a source can override a built-in icon. - const contextSource = useContext(IconGlyphSourceContext); - const resolved = resolveGlyph(contextSource, source, { + const resolved = useResolvedGlyph(source, { name, size, pixelSize: iconSize, diff --git a/packages/web/CHANGELOG.md b/packages/web/CHANGELOG.md index 515202b814..d11114c61a 100644 --- a/packages/web/CHANGELOG.md +++ b/packages/web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 9.29.0 (2026-09-23) + +This is an artificial version bump with no new change. + ## 9.28.0 (2026-09-18) ### 🚀 Features @@ -65,6 +69,10 @@ - Support font props on Tabs. [[#825](https://github.com/coinbase/cds/pull/825)] +## 9.23.0 ((8/26/2026, 09:18 AM PST)) + +This is an artificial version bump with no new change. + ## 9.22.0 (8/25/2026 PST) #### 🚀 Updates diff --git a/packages/web/package.json b/packages/web/package.json index 1e3b401f28..bb5ae8cef2 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,6 +1,6 @@ { "name": "@coinbase/cds-web", - "version": "9.28.0", + "version": "9.29.0", "description": "Coinbase Design System - Web", "repository": { "type": "git",