Skip to content

feat(a2ui): normalize icon names to Material Symbols ligatures (camelCase → snake_case)#725

Merged
blove merged 1 commit into
mainfrom
claude/a2ui-icon-showcase
Jun 22, 2026
Merged

feat(a2ui): normalize icon names to Material Symbols ligatures (camelCase → snake_case)#725
blove merged 1 commit into
mainfrom
claude/a2ui-icon-showcase

Conversation

@blove

@blove blove commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Follow-up to #720. The Material Symbols rendering shipped, but the live smoke found it wasn't actually showcased: A2UI catalogs emit camelCase icon identifiers (accountCircle, shoppingCart, moreVert — see the enum in examples/chat's a2ui schema), while Material Symbols ligatures are snake_case (account_circle). So single-word names (check, star) rendered as glyphs but the multi-word camelCase ones rendered as raw text.

This adds a small toMaterialSymbolName normalizer in the Icon component (camelCase → snake_case before rendering), so the existing A2UI icon vocabulary — and any backend — renders real glyphs. Single-word / already-snake_case names and emoji pass through unchanged; the original name stays as the aria-label.

Changes (libs/chat only)

  • a2ui/catalog/icon.component.ts — exported toMaterialSymbolName(); template renders the normalized glyphName(), keeps aria-label = original name.
  • icon.component.spec.ts — unit tests for the normalizer (camelCase→snake_case, passthrough, emoji).

Verification

  • chat test + lint + build green.
  • Live (examples/chat, A2UI mode): an "Account" card driven via the LLM rendered five real Material Symbols glyphsaccountCircle→account_circle, shoppingCart→shopping_cart, settings, notifications, star (each 18×18 square = glyph, not text). Screenshot in the PR thread.

Disjoint from the subagent-card work (#721).

🤖 Generated with Claude Code

…Case → snake_case)

A2UI catalogs commonly emit camelCase icon identifiers (accountCircle,
shoppingCart, moreVert) while Material Symbols ligatures are snake_case. The
Icon component now converts the name before rendering so the existing A2UI
icon vocabulary renders as real glyphs (not raw text). Single-word /
already-snake_case names + emoji pass through unchanged; the original name is
kept as the aria-label. Verified live in examples/chat (A2UI mode): an Account
card renders account_circle / shopping_cart / settings / notifications / star
glyphs.
@vercel

vercel Bot commented Jun 22, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
threadplane Ready Ready Preview, Comment Jun 22, 2026 3:41am

Request Review

@blove blove enabled auto-merge (squash) June 22, 2026 03:38

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Automated approval: this PR received an intelligent (AI) code review. See the review comments on this PR.

@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Claude finished @blove's task in 1m 4s —— View job


Code Review

  • Read changed files and diff
  • Analyze correctness, type safety, Angular patterns, and test coverage

LGTM. This is a clean, minimal fix. A few notes:

Correctness — toMaterialSymbolName regex

/([a-z0-9])([A-Z])/g with $1_$2 + .toLowerCase() is the canonical camelCase→snake_case transform. Spot-checked edge cases:

  • accountCircleaccount_circle
  • html5Canvashtml5_canvas (digit–uppercase boundary is also caught because [a-z0-9]) ✓
  • PascalCase input like AccountCircle — the first uppercase A has no preceding lowercase letter, but the t→C boundary is still caught: Account_Circleaccount_circle
  • Already-snake_case / single-word / emoji — no uppercase letters to match, pass through unchanged ✓

The .toLowerCase() at the end is necessary (handles PascalCase leading character) and correct.

effectiveName() always returns string, never undefined

name = input<string | undefined>(undefined) and icon = input<string>(''), so name() ?? icon() always resolves to a string. toMaterialSymbolName therefore never receives undefined. No null-safety gap.

Template — aria-label kept on original

[attr.aria-label]="name"   
>{{ glyphName() }}</span>  

Preserving the original name as the accessible label is correct — screen readers don't need the ligature string, they need a human-readable description.

Test coverage

Three cases cover the important paths (camelCase→snake_case, passthrough, emoji). One gap worth considering: PascalCase input (e.g. AccountCircle). It works correctly per the regex analysis above, but isn't explicitly tested. Not blocking — Material Symbols backends emit camelCase not PascalCase — but worth a future test case for documentation. Add it →

Minor observation

toMaterialSymbolName is exported from the component file. Works fine and is typical for Angular component helpers that need unit testing. Not worth moving.

No correctness bugs, no type-safety issues, no Angular/RxJS pitfalls. Ready to merge.


@blove blove merged commit 6580abb into main Jun 22, 2026
8 checks passed
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