diff --git a/renderers/angular/src/lib/catalog/icon.ts b/renderers/angular/src/lib/catalog/icon.ts index 770420d31..86148357b 100644 --- a/renderers/angular/src/lib/catalog/icon.ts +++ b/renderers/angular/src/lib/catalog/icon.ts @@ -17,6 +17,7 @@ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; import { DynamicComponent } from '../rendering/dynamic-component'; import * as Primitives from '@a2ui/web_core/types/primitives'; +import { toSnakeCase } from '@a2ui/web_core/styles/icons'; @Component({ selector: 'a2ui-icon', @@ -45,5 +46,8 @@ import * as Primitives from '@a2ui/web_core/types/primitives'; }) export class Icon extends DynamicComponent { readonly name = input.required(); - protected readonly resolvedName = computed(() => this.resolvePrimitive(this.name())); + protected readonly resolvedName = computed(() => { + const name = this.resolvePrimitive(this.name()); + return name ? toSnakeCase(name) : name; + }); } diff --git a/renderers/lit/src/0.8/ui/icon.ts b/renderers/lit/src/0.8/ui/icon.ts index aec35da79..1aa4d88ba 100644 --- a/renderers/lit/src/0.8/ui/icon.ts +++ b/renderers/lit/src/0.8/ui/icon.ts @@ -22,6 +22,7 @@ import * as Primitives from "@a2ui/web_core/types/primitives"; import { classMap } from "lit/directives/class-map.js"; import { styleMap } from "lit/directives/style-map.js"; import { structuralStyles } from "./styles.js"; +import { toSnakeCase } from "@a2ui/web_core/styles/icons"; @customElement("a2ui-icon") export class Icon extends Root { @@ -68,8 +69,7 @@ export class Icon extends Root { } const render = (url: string) => { - url = url.replace(/([A-Z])/gm, "_$1").toLocaleLowerCase(); - return html`${url}`; + return html`${toSnakeCase(url)}`; }; if (this.name && typeof this.name === "object") { diff --git a/renderers/react/src/components/content/Icon.tsx b/renderers/react/src/components/content/Icon.tsx index be8a607ee..bf486410a 100644 --- a/renderers/react/src/components/content/Icon.tsx +++ b/renderers/react/src/components/content/Icon.tsx @@ -19,15 +19,7 @@ import type * as Types from '@a2ui/web_core/types/types'; import type {A2UIComponentProps} from '../../types'; import {useA2UIComponent} from '../../hooks/useA2UIComponent'; import {classMapToString, stylesToObject} from '../../lib/utils'; - -/** - * Convert camelCase to snake_case for Material Symbols font. - * e.g., "shoppingCart" -> "shopping_cart" - * This matches the Lit renderer's approach. - */ -function toSnakeCase(str: string): string { - return str.replace(/([A-Z])/g, '_$1').toLowerCase(); -} +import {toSnakeCase} from '@a2ui/web_core/styles/icons'; /** * Icon component - renders an icon using Material Symbols Outlined font. diff --git a/renderers/web_core/src/v0_8/styles/icons.ts b/renderers/web_core/src/v0_8/styles/icons.ts index e62d7f509..404a86309 100644 --- a/renderers/web_core/src/v0_8/styles/icons.ts +++ b/renderers/web_core/src/v0_8/styles/icons.ts @@ -14,6 +14,21 @@ * limitations under the License. */ +/** + * Converts a camelCase icon name to snake_case for use with Material Symbols. + * + * The Material Symbols font uses snake_case names (e.g., "shopping_cart"), + * but A2UI icon names may be provided in camelCase (e.g., "shoppingCart"). + * This utility normalizes them so all renderers behave consistently. + * + * @example + * toSnakeCase('shoppingCart') // -> 'shopping_cart' + * toSnakeCase('home') // -> 'home' + */ +export function toSnakeCase(str: string): string { + return str.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, ''); +} + /** * CSS classes for Google Symbols. *