Skip to content

refactor(web/heroui): drop Button/Chip startContent + endContent (Phase 5)#13

Merged
dingyi merged 1 commit into
mainfrom
feature/heroui-v3-start-end-content
Apr 27, 2026
Merged

refactor(web/heroui): drop Button/Chip startContent + endContent (Phase 5)#13
dingyi merged 1 commit into
mainfrom
feature/heroui-v3-start-end-content

Conversation

@dingyi

@dingyi dingyi commented Apr 27, 2026

Copy link
Copy Markdown
Owner

Closes Phase 5 of web/TODO.md — the last open phase. After this lands
the only HeroUI v3 cleanup work that remains is downstream visual
review (no codemodable items left).

Summary

HeroUI v3 removed the startContent / endContent props on Button &
Chip. Icons now have to live in children, with the component's
built-in gap-2 doing the spacing. Without this rewrite the props
were silently ignored — ~135 buttons across the app rendered without
their leading or trailing icon.

The migration is mechanical:

<Button startContent={<X />}>label</Button>
↓
<Button>
  <X />
  label
</Button>

So we ship it as a one-shot codemod —
web/scripts/heroui-v3-startend-content-codemod.mjs is idempotent
(reports 0 edits on a clean tree). It runs @babel/parser over every
.jsx / .tsx file under web/src and:

  • finds JSXAttribute nodes named startContent / endContent;
  • deletes the attribute (also swallowing surrounding whitespace so we
    don't leave blank lines);
  • injects the attribute's expression as the first / last child of
    the surrounding JSX element;
  • converts self-closing elements (<Foo startContent={<X/>} />) to
    children form (<Foo><X/></Foo>);
  • wraps non-JSX expressions in { ... } so they live inside a JSX
    expression container instead of being parsed as plain text — this
    otherwise would have silently broken the custom-OAuth login button
    (getOAuthProviderIcon(provider.icon, 20)).

55 files / 135 attribute edits.

Verification

$ rg "startContent=|endContent=" web/src/
(no matches)

$ bun scripts/heroui-v3-startend-content-codemod.mjs
files touched: 0
attribute edits: 0

ESLint goes from 94 errors on origin/main to 94 (no regression — the
deleted props were silently ignored, not lint-flagged).

Conflict notes

This branch was created off origin/main independently of PR #10 / #11.
Five files overlap with PR #10 (CheckinCalendar, ModelTestModal,
MultiKeyManageModal, UserSubscriptionsModal, SettingsChannelAffinity)
and six with PR #11 (DeploymentAccessGuard, CustomOAuthSetting, four
Settings/Dashboard files). The codemod is idempotent, so the cleanest
resolution is: merge #10 / #11 first, then re-run the codemod on the
updated main and force-push this branch — rather than hand-resolving
135 mechanical edits.

Test plan

  • bun run dev — every Button / Chip that used to declare an
    icon prop still shows it.
  • Auth pages — Login / Register show the correct social provider
    icons (custom OAuth one was the trickiest case).
  • Settings → Channel Affinity / Performance / Chat / Dashboard
    sub-tabs — buttons show + / save / chevron icons as before.
  • Re-run codemod on a clean tree → 0 edits.

This PR was generated automatically by Cursor.

Made with Cursor

…se 5)

HeroUI v3 removed the startContent / endContent props on Button & Chip.
Icons now have to live in `children`, with the component's built-in
`gap-2` doing the spacing. Without this rewrite the props were silently
ignored — ~135 buttons across the app rendered without their leading
or trailing icon.

Migration is mechanical:

  <Button startContent={<X />}>label</Button>
  →
  <Button>
    <X />
    label
  </Button>

  <Button endContent={<Y />}>label</Button>
  →
  <Button>
    label
    <Y />
  </Button>

So we ship it as a one-shot codemod (idempotent — `bun scripts/heroui-v3-startend-content-codemod.mjs` reports 0 edits on a clean tree). 55 files / 135 attribute edits.

The codemod uses @babel/parser + @babel/traverse to walk every JSX file
under web/src and:

- finds JSXAttribute nodes named startContent / endContent;
- deletes the attribute (also swallowing its surrounding whitespace
  so we don't leave blank lines);
- injects the attribute's expression as the first / last child of
  the surrounding JSX element.

Special cases handled:

- Self-closing elements (`<Foo startContent={<X/>} />`) are converted
  to children form (`<Foo><X/></Foo>`).
- Non-JSX expressions (e.g. `getOAuthProviderIcon(provider.icon, 20)`)
  are wrapped in `{ ... }` so they live inside a JSX expression
  container instead of being parsed as plain text — this otherwise
  would have silently broken the custom-OAuth login button.

Verification: `rg "startContent=|endContent=" web/src/` is empty.
Made-with: Cursor
@dingyi
dingyi force-pushed the feature/heroui-v3-start-end-content branch from 4f42d34 to 58cde6a Compare April 27, 2026 18:28
@dingyi
dingyi merged commit 51d457c into main Apr 27, 2026
@dingyi
dingyi deleted the feature/heroui-v3-start-end-content branch April 27, 2026 18:28

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4f42d34992

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

traverse(ast, {
JSXAttribute(path) {
const name = path.node.name?.name;
if (name !== 'startContent' && name !== 'endContent') return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Scope start/end codemod to Button and Chip only

This rewrite currently matches any JSX attribute named startContent/endContent without checking the host component, so it also transformed non-Button usage (for example Input in TopupHistoryModal.jsx and OllamaModelModal.jsx). Phase 5 and this script’s own header describe the migration as Button/Chip-specific; applying it globally converts valid slot props into plain children, which can drop adornments or conflict with Input’s expected child structure (Input.Control/Input.Element). Please gate the transform by the JSX element name before editing attributes.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant