Skip to content

Commit 6580abb

Browse files
authored
feat(a2ui): normalize icon names to Material Symbols ligatures (camelCase → snake_case) (#725)
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.
1 parent cbe12f4 commit 6580abb

2 files changed

Lines changed: 40 additions & 2 deletions

File tree

libs/chat/src/lib/a2ui/catalog/icon.component.spec.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// SPDX-License-Identifier: MIT
22
import { describe, it, expect } from 'vitest';
3-
import { A2uiIconComponent } from './icon.component';
3+
import { A2uiIconComponent, toMaterialSymbolName } from './icon.component';
44

55
describe('A2uiIconComponent', () => {
66
// Display-only component: renders name() input as a <span>.
@@ -11,3 +11,24 @@ describe('A2uiIconComponent', () => {
1111
expect(A2uiIconComponent).toBeDefined();
1212
});
1313
});
14+
15+
describe('toMaterialSymbolName', () => {
16+
it('converts camelCase identifiers to snake_case ligatures', () => {
17+
expect(toMaterialSymbolName('accountCircle')).toBe('account_circle');
18+
expect(toMaterialSymbolName('shoppingCart')).toBe('shopping_cart');
19+
expect(toMaterialSymbolName('moreVert')).toBe('more_vert');
20+
expect(toMaterialSymbolName('visibilityOff')).toBe('visibility_off');
21+
expect(toMaterialSymbolName('arrowForward')).toBe('arrow_forward');
22+
});
23+
24+
it('passes single-word and already-snake_case names through unchanged', () => {
25+
expect(toMaterialSymbolName('check')).toBe('check');
26+
expect(toMaterialSymbolName('star')).toBe('star');
27+
expect(toMaterialSymbolName('trending_up')).toBe('trending_up');
28+
});
29+
30+
it('leaves non-identifier glyphs (emoji) untouched', () => {
31+
expect(toMaterialSymbolName('✓')).toBe('✓');
32+
expect(toMaterialSymbolName('⚠️')).toBe('⚠️');
33+
});
34+
});

libs/chat/src/lib/a2ui/catalog/icon.component.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,20 @@
22
import { Component, computed, input } from '@angular/core';
33
import type { Spec } from '@json-render/core';
44

5+
/**
6+
* Convert an icon identifier to its Material Symbols ligature form.
7+
*
8+
* Material Symbols ligatures are snake_case (`account_circle`, `trending_up`),
9+
* but A2UI catalogs commonly emit camelCase identifiers (`accountCircle`,
10+
* `shoppingCart`). Splitting on lower→upper boundaries and lowercasing maps
11+
* camelCase → the matching ligature. Already-snake_case names, single words,
12+
* and non-identifier glyphs (emoji) have no boundaries to split and pass
13+
* through unchanged. Unknown names still fall back to the browser default.
14+
*/
15+
export function toMaterialSymbolName(name: string): string {
16+
return name.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
17+
}
18+
519
@Component({
620
selector: 'a2ui-icon',
721
standalone: true,
@@ -12,7 +26,7 @@ import type { Spec } from '@json-render/core';
1226
[style.font-size]="size() ? size() + 'px' : '1.125rem'"
1327
[attr.aria-label]="name"
1428
role="img"
15-
>{{ name }}</span>
29+
>{{ glyphName() }}</span>
1630
}
1731
`,
1832
styles: [`
@@ -56,4 +70,7 @@ export class A2uiIconComponent {
5670
readonly spec = input<Spec | undefined>(undefined);
5771

5872
protected readonly effectiveName = computed(() => this.name() ?? this.icon());
73+
74+
/** The effective name as a Material Symbols ligature (camelCase → snake_case). */
75+
protected readonly glyphName = computed(() => toMaterialSymbolName(this.effectiveName()));
5976
}

0 commit comments

Comments
 (0)