Skip to content

feat(icons): Sync the icon set with Figma - #2161

Merged
juliajforesti merged 9 commits into
mainfrom
feat/icons-figma-sync
Aug 19, 2026
Merged

feat(icons): Sync the icon set with Figma#2161
juliajforesti merged 9 commits into
mainfrom
feat/icons-figma-sync

Conversation

@ivans-netto

@ivans-netto ivans-netto commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Supersedes #2159 (closed automatically when its branch was renamed).

Summary

Task

Brings the @rocket.chat/icons set in sync with the Iconography Figma file, which is the design source of truth:

  • Adds 36 icons — the whole Benched icons frame, plus the icons documented on the "Fuselage" frame that never landed in the package (arrow-left, arrow-right, arrow-up-down, the chevron -big family, jump-backward/jump-forward and attachment-file-filled).
  • Updates 18 icons to the redrawn ([redraw]-prefixed) versions from the same frame — in the package and on the Figma "Fuselage" frame, whose corresponding components were replaced with the same geometry.
  • Fixes a glyph-allocation bug in build-icons that made adding more than one icon at a time unsafe.

All geometry is taken straight from Figma and reduced to the package's convention (viewBox + xmlns only, <path> elements keeping just d/fill-rule/clip-rule). Nothing that exists in main is renamed or removed, and no codepoint changes — for consumers this release is purely additive plus visual refreshes.

Visual overview

Added and updated icons overview

Added (36)

archive arrow-left arrow-right arrow-up-down attachment-file-filled bell-dot chevron-down-big chevron-left-big chevron-right-big chevron-up-big closed-captions folder-plus hand hash-lock hd hd-filled image-resize jump-backward jump-forward move note prune resize search-sparkle sidebar square-arrow-down-left square-arrow-up-right square-arrows-contained-inwards square-arrows-contained-outwards square-slash star-plus tag-horizontal tag-horizontal-filled tag-horizontal-plus voicemail volume-question-mark

folder-plus, note, sidebar, arrow-left, arrow-right, chevron-left-big and chevron-right-big are .dir (RTL-mirrored) to match their existing counterparts. jump-backward/jump-forward are not mirrored so their 15 numeral keeps its reading direction.

Updated (18)

arrow-expand attachment-file folder hash hashtag-lock mic mic-off rec team team-lock video video-disabled video-message video-off volume volume-disabled volume-lock volume-off

Geometry only. The previous versions were drawn on an older grid and carried off-grid coordinates; the redraws are pixel-aligned on the 32×32 box. For example hash moved off half units (10.510) and video off 0.444 units.

The phone family (phone, phone-disabled, phone-in, phone-issue, phone-off, phone-out) and pin are marked [redraw] in Figma too, but their redraws already shipped in #1735/#1882 — the bench exports differ from the package only by sub-0.001px float rounding (or not at all), so they are untouched. Verified numerically: same coordinate count, max deviation 0.0003px.

Figma updates applied alongside this PR

  • The 23 components on the "Fuselage" frame matching a [redraw] bench icon were replaced with the redrawn geometry (phone×6, team×2, hash, hashtag-locked, arrow-expand, mic×2, volume×4, pin, video×4, folder), so the library and the package stay in sync.
  • Bench components that duplicated published icons were renamed to the published names (stacked-meatballs, circle-unfilled, arrow-to-square-box, arrow-from-cross-box, rec, hd, hd-filled); tag-horizontal-plus-2 was deleted.

Deduplication against the published set

A pairwise shape-similarity sweep across all 356 SVGs (exact pixel IoU + a shift-tolerant pass), reviewed with design, caught seven bench icons that duplicate already-published ones. Resolution, validated by design:

Bench icon Published icon Resolution
pop-out-window arrow-to-square-box (#1958) byte-identical — not added; Figma component renamed
close-pop-out-window arrow-from-cross-box (#1958) byte-identical — not added; Figma component renamed
drag-handle stacked-meatballs (#1626) byte-identical — not added; Figma component renamed
circle-unfilled-medium circle-unfilled renders identically — not added; Figma component renamed
rec-unfilled rec same icon — rec updated to the redrawn geometry; Figma component renamed
video-arrow-up-right / video-arrow-down-left removed from the bench by design during review — dropped from the PR
hash-lock hashtag-lock same concept — hash-lock added as the canonical name and hashtag-lock updated to the same geometry, so both names render identically

Other renames applied for convention (star/star-filled pattern): hd-stroke/hd-fillhd/hd-filled, in Figma too. tag-horizontal-plus-2 was dropped by design and its Figma component deleted.

Pre-existing near-duplicates that are not touched by this PR, flagged for a future pass: list-bullets × medium-view (~97% overlap) and error-circle × circle-cross (same concept, filled/outline pair with non-standard names).

The build-icons fixes

Adding more than one icon at a time was unsafe. Three separate commits, one per cause:

  1. Await the glyphsMapping.json writewriteJson returned its data without awaiting the underlying write, so await writeJson(...) resolved before the file hit disk and the next reader could still observe the previous content.

  2. Serialize glyph character assignmentscreateSvgBuffer calls nextCharactersFor for every icon in parallel, and each call is a read-modify-write over glyphsMapping.json: read the mapping, derive the next free character from the current maximum, write it back. Concurrent calls read the same state, derived the same "next" character, and the last write clobbered the others' entries — adding N icons at once persisted only one of them and assigned duplicate codepoints. I hit this directly: the first build of this branch persisted 1 of 34 entries. The fix chains the assignments on a module-level promise, so they run strictly one at a time and each observes the previous one's result:

    let pendingAssignment = Promise.resolve();
    
    export const nextCharactersFor = (name, type) => {
      const assignment = pendingAssignment.then(() =>
        assignCharactersFor(name, type),
      );
      pendingAssignment = assignment.catch(() => undefined);
      return assignment;
    };

    The catch keeps the chain alive when an assignment fails — otherwise one rejection would wedge every later assignment — while the failed call's own caller still receives the original rejection, since assignment is returned before the catch.

  3. Write font glyphs in deterministic order — glyphs were written into the font stream from inside a Promise.all, so write order followed I/O completion order and varied between builds. For the pre-existing duplicated codepoint U+E147 (stacked-meatballs/rocketchat) that made the font itself non-deterministic: which glyph won the codepoint changed from build to build, flipping both cells in the Icon grid story — this is why the visual-regression check was bistable on this PR (failing with exactly 398 differing pixels, or passing, on identical input). Sources are still read in parallel, but glyphs are now written sequentially in the already-sorted icons order; the generated SVG font is byte-identical across builds and U+E147 consistently renders stacked-meatballs.

  4. Assign characters before generating the modulesbuildFont (which assigns characters to new icons) ran in the same Promise.all as the module builders (which read the mapping), so the first build after adding an icon silently left it out of index.js/index.mjs/index.d.ts while still emitting it into the SVG sprite; only a second build picked it up.

A single yarn build now assigns everything and is idempotent.

Note

This bug already bit main: stacked-meatballs and rocketchat both map to U+E147. I left that alone here because changing either codepoint is a breaking change for anyone rendering the font directly — happy to fix it in a follow-up, just needs a call on which one moves.

Verification

  • yarn build in packages/icons succeeds; all 36 new icons resolve in index.js, index.mjs, index.d.ts, dist/icons.svg and dist/svg/.
  • No name that exists in main was removed and no codepoint that exists in main changed (verified by diffing glyphsMapping.json against main).
  • Codepoints are unique across all exported names (except the pre-existing U+E147 pair above).
  • Rebuild produces no further change to glyphsMapping.json (idempotent).
  • hash-lock and hashtag-lock render byte-identically in the build output.
  • The 7 new .dir icons and the 2 modified .dir icons carry rcx-svg--directional in the build output.
  • Every added and updated icon was rendered from the built dist/svg/ output and visually checked; the redraws were also compared against the current versions pixel-by-pixel to confirm each difference is a real design change rather than export noise.
  • Visual-regression snapshots updated (Icon grids, Tag, SidebarV2/Item, Sidepanel and Sidebar stories). Regenerated in the pinned playwright:v1.62.0-noble image so they match CI; the full suite is 506/506 green.
  • The Icon Default story renders the whole set through the icon font, so its snapshot doubles as end-to-end proof that every new codepoint resolves to a real glyph and none render blank.
  • yarn lint passes for packages/iconseslint, stylelint, and prettier (which does check SVGs here, via @prettier/plugin-xml).

🤖 Generated with Claude Code

@changeset-bot

changeset-bot Bot commented Aug 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 93b898e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@rocket.chat/icons Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@ivans-netto
ivans-netto force-pushed the feat/icons-figma-sync branch 5 times, most recently from 6db5737 to 478c0fd Compare August 19, 2026 16:59
Comment thread .changeset/icons-figma-sync-batch.md Outdated
@ivans-netto
ivans-netto force-pushed the feat/icons-figma-sync branch from 478c0fd to e20118e Compare August 19, 2026 18:12
@ivans-netto
ivans-netto dismissed juliajforesti’s stale review August 19, 2026 18:25

reviewed already addressed

ivans-netto and others added 8 commits August 19, 2026 15:33
`writeJson` returned the data without awaiting the underlying write, so
`await writeJson(...)` in `nextCharactersFor` resolved before the file was
actually on disk and the next reader could still observe the previous
content.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`createSvgBuffer` calls `nextCharactersFor` for every icon in parallel, and
each call is a read-modify-write over `glyphsMapping.json`: read the
mapping, derive the next free character from the current maximum, write the
mapping back. Concurrent calls read the same state, derive the same "next"
character, and the last write clobbers the entries of the others - adding N
icons at once persisted only one of them and assigned duplicate codepoints.
`stacked-meatballs` and `rocketchat` share U+E147 in `main` because of this.

Chain the assignments on a module-level promise so they run one at a time
and each observes the previous one's result. The chain swallows rejections
so one failed assignment cannot wedge the ones that follow, while the
caller of the failed assignment still receives the original rejection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Glyphs were written into the font stream from inside a `Promise.all`, so
the write order followed I/O completion order and varied between builds.
For the duplicated codepoint U+E147 (`stacked-meatballs`/`rocketchat`) that
made the font itself non-deterministic - which glyph won the codepoint
changed from build to build, flipping both grid cells in the Icon story and
making the visual-regression check bistable (fail with exactly 398 differing
pixels, or pass, on identical input).

Read the sources in parallel but write the glyphs sequentially in the
already-sorted icons order. The generated SVG font is now byte-identical
across builds; U+E147 consistently renders `stacked-meatballs`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`buildFont` (which assigns characters to new icons) ran in the same
`Promise.all` as the module builders (which read the mapping), so the first
build after adding an icon silently left it out of the generated
`index.js`/`index.mjs`/`index.d.ts` while still emitting it into the SVG
sprite; only a second build picked it up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All bench icons that were not already published, plus the icons documented
on the "Fuselage" frame that never landed in the package (`arrow-left`,
`arrow-right`, `arrow-up-down`, the chevron `-big` family,
`jump-backward`/`jump-forward` and `attachment-file-filled`), after
deduplicating against the existing set:

- `pop-out-window`, `close-pop-out-window`, `drag-handle` and
  `circle-unfilled-medium` were not added - they render identically to the
  published `arrow-to-square-box`, `arrow-from-cross-box`,
  `stacked-meatballs` and `circle-unfilled`.
- `hd-stroke`/`hd-fill` landed as `hd`/`hd-filled`, following the
  `star`/`star-filled` naming convention.
- `tag-horizontal-plus-2` was dropped by design.
- The Figma components were renamed/removed accordingly.

`folder-plus`, `note`, `sidebar`, `arrow-left`, `arrow-right`,
`chevron-left-big` and `chevron-right-big` are `.dir` (RTL-mirrored) to
match their existing counterparts. `jump-backward`/`jump-forward` are not
mirrored so their `15` numeral keeps its reading direction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Geometry only - no icon is renamed or removed. The previous versions were
drawn on an older grid and carried off-grid coordinates (`hash` sat on half
units, `video` on 0.444 units); the redraws are pixel-aligned on the 32x32
box.

`hashtag-lock` takes the same redrawn geometry as the new `hash-lock`, and
`attachment-file` the same redraw as the new `attachment-file-filled`, so
each concept renders in a single style.

The phone family and `pin` are marked `[redraw]` in Figma too, but their
redraws already shipped in #1735/#1882 - the exports differ from the
package only by sub-0.001px float rounding, so they are untouched.

The corresponding components on the Iconography Figma file's "Fuselage"
frame were replaced with the same redrawn geometry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Júlia Jaeger Foresti <60678893+juliajforesti@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Icon grids pick up the added and redrawn glyphs, and the Tag, SidebarV2,
Sidepanel and Sidebar stories render redrawn icons. Regenerated in the
pinned playwright:v1.62.0-noble image so the output matches CI; 506/506
pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ivans-netto
ivans-netto force-pushed the feat/icons-figma-sync branch from 2a91a4d to eebbe78 Compare August 19, 2026 18:33
@mergify

mergify Bot commented Aug 19, 2026

Copy link
Copy Markdown

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

@juliajforesti
juliajforesti merged commit fb7bd1a into main Aug 19, 2026
7 checks passed
@juliajforesti
juliajforesti deleted the feat/icons-figma-sync branch August 19, 2026 18:51
@github-actions github-actions Bot mentioned this pull request Aug 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants