refactor(frontend): replace Phosphor icons with Hugeicons stroke-rounded - #13751
Conversation
Swap the entire icon set from @phosphor-icons/react to Hugeicons
stroke-rounded, rendered through a new `Icon` atom that applies a
uniform 1.75px stroke width.
Phosphor icons are React components; Hugeicons ship as icon *data*
(`IconSvgElement`), so every call site moves from `<TrashIcon />` to
`<Icon icon={Delete02Icon} />` and every icon-carrying prop/record is
retyped from a component type to `IconSvgElement`.
- Add src/components/atoms/Icon/Icon.tsx — wraps HugeiconsIcon, defaults
strokeWidth to 1.75 and size to "1em" (matching Phosphor's sizing).
Also exports createIconComponent() for records that mix Hugeicons with
react-icons / radix components.
- Map all 291 distinct Phosphor icons to Hugeicons equivalents and
rewrite 852 JSX sites and 321 value/type references across 382 files.
- Drop the Phosphor `weight` prop everywhere; Hugeicons free is stroke
only, and stroke width is now owned by the Icon atom. Where `weight`
encoded state (connected node handles) it is replaced by `fill`.
- Rename getAccountMenuPhosphorIcon to getAccountMenuIcon and drop its
now-unused weight argument.
- Update the icon catalog story, test mocks, AGENTS/CONTRIBUTING/README
and copilot instructions to the new convention.
- Remove the @phosphor-icons/react dependency.
|
Important Review skippedToo many files! This PR contains 405 files, which is 105 over the limit of 300. To get a review, reduce the PR to 300 files or fewer by splitting it into smaller PRs or changing its base branch. Usage-priced reviews support at most 300 files. ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (405)
You can disable this status message by setting the Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## dev #13751 +/- ##
==========================================
+ Coverage 76.92% 77.04% +0.12%
==========================================
Files 2761 2762 +1
Lines 209521 209513 -8
Branches 20077 20132 +55
==========================================
+ Hits 161171 161421 +250
+ Misses 43981 43634 -347
- Partials 4369 4458 +89
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
Two files had "use client" followed by a single Phosphor import. When the codemod removed that import, the new Hugeicons imports were inserted above the directive, demoting it to a plain expression statement and breaking the webpack build. Both were reported by eslint as `no-unused-expressions` warnings rather than errors, so they were missed in the initial pass.
Mounting <Wallet /> also mounts <TaskGroups />, whose celebration effect arms a real 300ms setTimeout that fires two confetti bursts. updateState is stubbed in this suite, so `notified` never updates and the timer is re-armed on every render. That timer outlives the test that armed it and lands in the gap before a later test body, inflating confettiMock's call count. The file runs in ~200ms locally so the timer never landed; on slower CI it did, failing `expected "vi.fn()" to not be called at all, but actually been called 4 times`. Clear confettiMock after render, alongside the existing fetchCreditsMock.mockClear(), so each assertion starts from a clean slate. The span from clear to assert is synchronous, so no timer can interleave.
The codemod rewrote `return <Icon icon={X} />` across several switch-based
icon pickers that had no tests, dropping Codecov patch coverage to 49%.
A wrong or undefined icon import renders an empty <svg> rather than
throwing, so these assert on the drawn path geometry: every branch renders
a glyph, and categories that should look different actually do. That is
what catches a bad mapping; asserting the element merely exists would not.
Covers ToolIcon/AccordionIcon in GenericTool, EntryIcon in
CollapsedToolGroup, getSocialIcon in CreatorLinks, and
ToolStatusIcon/getAccordionIcon in FolderTool.
Patch coverage 49.31% -> 81.94%.
|
!deploy |
|
🚀 Deploying PR #13751 to development environment... |
|
✅ Preview environment is live (all services healthy)
Push more commits, then comment |
|
🔄 Auto-redeploying: new commits pushed to a PR with an active deployment. Refreshing development environment for PR #13751. |
|
🔄 Auto-redeploying: new commits pushed to a PR with an active deployment. Refreshing development environment for PR #13751. |
|
🔄 Auto-redeploying: new commits pushed to a PR with an active deployment. Refreshing development environment for PR #13751. |
|
🔄 Auto-redeploying: new commits pushed to a PR with an active deployment. Refreshing development environment for PR #13751. |
🔍 PR Overlap DetectionThis check compares your PR against all other open PRs targeting the same branch to detect potential merge conflicts early. 🔴 Merge Conflicts DetectedThe following PRs have been tested and will have merge conflicts if merged after this PR. Consider coordinating with the authors.
🟢 Low Risk — File Overlap OnlyThese PRs touch the same files but different sections (click to expand)
Summary: 7 conflict(s), 0 medium risk, 5 low risk (out of 12 PRs with file overlap) Auto-generated on push. Ignores: |
There was a problem hiding this comment.
Re-approved at the request of @Abhi1992002 (#13751 (comment))
|
🧹 Auto-undeploying: PR closed with active deployment. Cleaning up development environment for PR #13751. |
|
🧹 Preview Environment Cleaned Up All resources for PR #13751 have been removed:
Cleanup completed successfully. |
Why / What / How
Why: We're standardising on Hugeicons stroke-rounded as the platform icon set. Phosphor was the previous standard and was used in 382 frontend files, so the switch has to be done in one pass — a partial migration would leave two icon systems with visibly different stroke weights side by side.
What: Every Phosphor icon in the frontend is replaced by its Hugeicons stroke-rounded equivalent, rendered through a new
Iconatom that applies a uniform 2px stroke width.@phosphor-icons/reactis removed frompackage.json.How: Phosphor icons are React components; Hugeicons ship as icon data (
IconSvgElement— an array of SVG path tuples) rendered by a single component. That difference drives the shape of the whole diff:The mechanical rewrite was done with a
ts-morphAST codemod (not regex — many Phosphor exports are common words likeCheck,File,X,Table, and a textual replace would have corrupted strings and JSX text). It rewrote 852 JSX sites and 321 value/type references. The remaining ~135 sites — places where an icon was stored in a variable or record and rendered indirectly — were fixed by hand, since they need a real decision about whether the field should becomeIconSvgElementor stay a component type.Stroke width lives in exactly one place (
ICON_STROKE_WIDTHin theIconatom) rather than being repeated on ~850 call sites, so changing it later is a one-line edit.Changes 🏗️
Iconatom (src/components/atoms/Icon/Icon.tsx) — wrapsHugeiconsIcon, defaultsstrokeWidthto1.75andsizeto"1em"so icons keep scaling with surrounding text exactly as Phosphor's did. Also exportscreateIconComponent()for the few records that legitimately mix Hugeicons withreact-icons/@radix-ui/react-iconscomponents and therefore need a uniform component type.TrashIcon→Delete02Icon,CaretDownIcon→ArrowDown01Icon,CircleNotchIcon→Loading03Icon,MagnifyingGlassIcon→Search01Icon).weightprop dropped everywhere (389 usages). Hugeicons free is stroke-only and stroke width is now owned by the atom. In the one place whereweightencoded state rather than style — connected vs unconnected node handles in the builder — it's replaced byfill={isInputConnected ? "currentColor" : "none"}so the distinction survives.IconSvgElement(nav item configs, artifact classification, sitrep priority config, etc.).getAccountMenuPhosphorIcon→getAccountMenuIcon, dropping its now-unusedweightargument.frontend/AGENTS.md,frontend/CONTRIBUTING.md,frontend/README.md,.github/copilot-instructions.md, and thecomponents/tokens/icons.stories.tsxcatalog story.@phosphor-icons/reactnow stub theIconatom; two of those mocks were dead (their testids were never asserted) and were removed.@phosphor-icons/reactremoved;@hugeicons/react+@hugeicons/core-free-iconsadded.Checklist 📋
For code changes:
pnpm types— clean, 0 errorspnpm lint— clean, 0 errorspnpm test:unit— 405 test files / 4237 tests pass, 2 skippedpnpm build— production build succeedsgit grep phosphorreturns nothing outside the lockfileVisual review
This is the part CI can't cover. The mapping is 1:1 in meaning but Hugeicons glyphs are not pixel-identical to Phosphor's, and Phosphor's
weight="fill"/"duotone"variants have no Hugeicons equivalent, so some previously-filled icons are now outlines. Worth a pass over:fillrather than icon weightTokens/Iconscatalog for a side-by-side of the whole setA few mappings were judgement calls where Hugeicons has no close twin —
ChalkboardIcon→Presentation01Icon,CardsThreeIcon→Album01Icon,FoldersIcon→FolderLibraryIcon,WarningOctagonIcon→Alert02Icon. Happy to swap any of these if a reviewer prefers a different glyph.For configuration changes:
.env.defaultis updated or already compatible with my changesdocker-compose.ymlis updated or already compatible with my changes