Text and Icon color rename codemod

Migrates the color values that were renamed in 5.0.0: the Text and Icon color props, the iconColor prop of the components that render an icon in a slot of their own, the Box backgroundColor sprinkle, and the Coin borderColor prop, along with the matching style-export keys.

The neutral text roles collapsed to a single primary/secondary pair and the surface and scope names replaced the old bold/subtle/minimal shading, so the values that only differed by shade are gone. Emphasis is a weight now (weight="semibold", or a product-*-active text style), not a darker gray.

// Before
<Text color="subtle">Last synced 2 minutes ago</Text>
<Icon name="help" color="navigation" />
<TextField icon="search" iconColor="subtle" />
<Box backgroundColor="overlay" />
<Coin borderColor="accent-minimal" />

// After
<Text color="secondary">Last synced 2 minutes ago</Text>
<Icon name="help" color="primary" />
<TextField icon="search" iconColor="secondary" />
<Box backgroundColor="floating" />
<Coin borderColor="accent-subtle" />

What it rewrites

Text color

table
Removed valueReplacementNotes
baseprimaryAlso the value an omitted color resolves to.
boldprimaryEmphasis is a weight now, not a darker gray.
activeprimarySelected and current states use weight or another cue.
subtlesecondary
minimalsecondaryminimal was near-invisible; secondary is a normal reading gray. Look over anywhere that relied on the old faintness.
placeholdersecondary

accent, caution, critical, information, success, invert, white, black, code, disabled, and inherit are unchanged.

Icon color

table
Removed valueReplacement
baseprimary
boldprimary
buttonprimary
navigationprimary
subtlesecondary
minimalsecondary

accent, caution, critical, feature, information, success, invert, white, disabled, and inherit are unchanged. An Icon with no color still takes the surrounding text color.

Text and Icon share the removed spellings but not every replacement — button and navigation exist only on Icon — so the codemod matches on the component, not the value.

iconColor

Combobox, TextField, DropdownMenuItem, DropdownMenuCheckboxItem, the Tabs and TabNav tab, and SelectPaletteCategory color the icon they render in a slot of their own through an iconColor prop. It takes the Icon union, so it uses the Icon table above:

table
Removed valueReplacement
baseprimary
boldprimary
buttonprimary
navigationprimary
subtlesecondary
minimalsecondary

Only the prop is rewritten. The same value inside an options, tabs, or categories array is data rather than an attribute and is left alone — see Limitations.

Button's unsafe_iconColor is a different union and is untouched.

Box backgroundColor

table
Removed valueReplacementNotes
basecontentThe plane content sits on. The new base is the page behind everything, so leaving this one alone would turn a white region gray — and rewriting one that was already migrated by hand turns it gray instead. Still valid in v5, so the rewrite is reported rather than silent: check each one.
subtleoverlay-tintReported. A faint region tint, one step off the surface it sits on; use neutral-subtle if this was a filled chip. On Avatar, Badge, Coin, StatusIndicator and Tag, whose backgroundColor fills a chip, subtle becomes neutral-subtle silently.
activeneutral-subtleWas a pressed-state layer, a step stronger than subtle; a static background is the neutral subtle fill.
overlay-activeneutral-subtleSame, and reported: a pressed layer has no Box value, so if the element shows a pressed or selected state, set backgroundColor: layerPressed() from @customerio/pluma-components/css/utils in its styles instead.
minimaloverlay-tintReported. A fainter tint than subtle that now shares its tint; use neutral-subtle if this was a filled chip, or content if the element should sit flush with the panel. On those chip components it becomes neutral-subtle, still reported.
navigationbaseNavigation sits on the page plane.
overlayfloating
invertinverseReported: 4.x invert was a dark teal and inverse is the neutral plane that flips with the theme. If the teal was intended, set backgroundColor: fillScope('action', 'solid') from @customerio/pluma-components/css/utils in its styles instead.
invert-boldinverseSame.
invert-subtleneutralThe solid neutral scope. Reported: 4.x invert-subtle was teal in dark.
accent-minimalaccent-subtle
caution-boldcaution
caution-minimalcaution-subtle
critical-boldcritical
critical-minimalcritical-subtle
information-boldinformation
information-minimalinformation-subtle
success-boldsuccess
success-minimalsuccess-subtle
boldremovedNo replacement — the attribute is deleted and reported.

accent, caution, critical, information, success and their -subtle twins are unchanged; neutral, neutral-subtle, feature, feature-subtle, block, content, floating, overlay-tint, recessed, and inverse are new.

A solid scope background sets the default text to that scope's solid text, a -subtle background to its subtle text, and inverse to the inverted text, so a Text inside one of these usually needs no color of its own.

Coin borderColor

table
Removed valueReplacement
baseneutral-subtle
minimalneutral-subtle
boldneutral
activeneutral
accent-minimalaccent-subtle
caution-minimalcaution-subtle
critical-minimalcritical-subtle
information-minimalinformation-subtle
success-minimalsuccess-subtle
invertremoved — the attribute is deleted and reported; a Coin with no borderColor paints no border.

Style exports

PlumaCoin.defaultVars from @customerio/pluma-components/css/components:

table
Removed keyReplacement
borderColorBaseborderColorNeutralSubtle
borderColorMinimalborderColorNeutralSubtle
borderColorBoldborderColorNeutral
borderColorActiveborderColorNeutral
borderColorAccentMinimalborderColorAccentSubtle
borderColorCautionMinimalborderColorCautionSubtle
borderColorCriticalMinimalborderColorCriticalSubtle
borderColorInformationMinimalborderColorInformationSubtle
borderColorSuccessMinimalborderColorSuccessSubtle
borderColorInvertremoved — reported, not rewritten.

Both PlumaCoin.defaultVars.borderColorBase and the destructured const { defaultVars } = PlumaCoin form are handled, as is a namespace import (import * as pluma from '…/css/components').

Components it covers

backgroundColor is rewritten on any component imported from a Pluma package — it is a Box sprinkle every one of them forwards. borderColor is scoped to Coin.

color is rewritten only where it is the Text or Icon union:

Text color — Avatar, CardTitle, Code, Coin, ConditionViewerDetail, ConditionViewerTerm, DescriptionList, DescriptionListDetails, DescriptionListTerm, Emphasis, EmptyStateDescription, EmptyStateFooter, EmptyStateTitle, FormattedDateTime, FormattedRelativeTime, Heading, KeyboardShortcut, List, ListItem, PageHeaderDescription, PageHeaderTitle, PanelDescription, PanelTitle, Paragraph, SkeletonText, Spinner, Strong, Text, TextLabel, and Truncated.

Icon color — Icon, PageHeaderIcon, and SelectMenuOptionIcon.

iconColor — Combobox, DropdownMenuCheckboxItem, DropdownMenuItem, SelectPaletteCategory, TabNavTab, TabsTab, and TextField.

The Ember tags are the same names with a Pluma prefix (<PlumaText @color="subtle">).

Deliberately left alone, because their color prop is a different union: Badge, StatusIndicator, TimelineItem, PageHeaderIconTile, Coin's colorVariant, ConditionViewerItem's iconColorVariant, and Button's unsafe_iconColor. Paragraph's variant prop is unchanged in 5.0.0.

What it flags instead of rewriting

Values it can't resolve to a literal are left as they are and collected in a "needs manual review" summary:

  • A color, iconColor, backgroundColor, or borderColor coming from an identifier, this.x, an @arg, a helper, or a call. Trace the binding — these often live in a model, constant, or type union in another file.
  • A partly-resolvable expression (color={isMuted ? 'subtle' : fallback}): the literal branches are migrated and the rest is reported.
  • The removals — backgroundColor="bold", Coin borderColor="invert", and defaultVars.borderColorInvert — because deleting a background or a ring is a visible change worth a second look.

Running it

Run it through the Pluma CLI from inside your consuming app. Point the React codemod at source directories (./src, ./app) rather than a workspace root — jscodeshift walks everything it is handed:

pnx @customerio/pluma-cli@latest upgrade --codemod text-icon-color-rename react ./src
pnx @customerio/pluma-cli@latest upgrade --codemod text-icon-color-rename ember ./app ./addon

Run the react command over your vanilla-extract .css.ts files as well, including in an Ember app — that is where the style-export keys live. The ember command walks .gts, .gjs, and .hbs.

Both support dry runs. The React codemod uses jscodeshift flags, so --dry/-d skips writes and -p prints the transformed output; the Ember codemod has its own --dry:

pnx @customerio/pluma-cli@latest upgrade --codemod text-icon-color-rename react --dry ./src
pnx @customerio/pluma-cli@latest upgrade --codemod text-icon-color-rename ember --dry ./app

A second run over files it migrated is a no-op. Every value it renames to but one is new in v5; the exception is backgroundColor="navigation", which becomes base, itself a 4.x value. The run ledger runs the transform over what it just wrote, records each base it wrote for navigation in .pluma/codemod-ledger.json, and a later run leaves those sites alone.

One value does not survive a partly hand-migrated file, though. backgroundColor="base" becomes "content", and base is still a valid v5 value (the page plane), so a base that someone already put there deliberately looks exactly like one this codemod has yet to migrate and will be rewritten. Every one of those rewrites is reported for that reason — work through them before committing.

That is also the case to reach for --baseline on a migrated branch that keeps merging pre-5.0 code in: before the merge, run it over the branch's own code to record every base already there as intended, and a later run migrates only the base the merge brought in:

pnx @customerio/pluma-cli@latest upgrade --codemod text-icon-color-rename react --baseline ./src

Order relative to the 3.0.0 codemods

There is no ordering constraint. The two codemods in this batch that emit new elements emit props this one doesn't read: label-icon-to-coin produces <Coin colorVariant> around a bare <Icon>, and label-to-badge produces <Badge color> with hue values that share no spelling with anything in the tables above. Both orders are pinned in the tests.

Limitations

  • Only angle-bracket / JSX invocations are handled. A color passed through a spread or a props object is not seen.
  • iconColor is only rewritten where it is an attribute. The same key inside a data array — a Select or Combobox option, a Select palette category, a Tabs or TabNav tabs entry, a Select option group — is an object property the transform doesn't reach, in either framework. Rename those by hand using the Icon table above.
  • DropdownMenuItem's trailingIconColor takes the same Icon union and is not covered.
  • Type imports are not rewritten, and neither is a binding destructured straight off a renamed key (const { borderColorBase } = PlumaCoin.defaultVars).
  • Generated class names moved with the keys (pluma-text-color-base became pluma-text-color-primary). Anything matching on those strings — a test selector, a hand-written stylesheet — is a manual fix.