Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion renderers/angular/src/lib/catalog/icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
import { toMaterialSymbolLigature } from '@a2ui/web_core/styles/icons';
import { DynamicComponent } from '../rendering/dynamic-component';
import * as Primitives from '@a2ui/web_core/types/primitives';

Expand Down Expand Up @@ -45,5 +46,8 @@ import * as Primitives from '@a2ui/web_core/types/primitives';
})
export class Icon extends DynamicComponent {
readonly name = input.required<Primitives.StringValue | null>();
protected readonly resolvedName = computed(() => this.resolvePrimitive(this.name()));
protected readonly resolvedName = computed(() => {
const resolvedName = this.resolvePrimitive(this.name());
return resolvedName ? toMaterialSymbolLigature(resolvedName) : null;
});
}
3 changes: 2 additions & 1 deletion renderers/lit/src/0.8/ui/icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { html, css, nothing } from "lit";
import { customElement, property } from "lit/decorators.js";
import { Root } from "./root.js";
import { A2uiMessageProcessor } from "@a2ui/web_core/data/model-processor";
import { toMaterialSymbolLigature } from "@a2ui/web_core/styles/icons";
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";
Expand Down Expand Up @@ -68,7 +69,7 @@ export class Icon extends Root {
}

const render = (url: string) => {
url = url.replace(/([A-Z])/gm, "_$1").toLocaleLowerCase();
url = toMaterialSymbolLigature(url);
return html`<span class="g-icon">${url}</span>`;
};

Expand Down
12 changes: 2 additions & 10 deletions renderers/react/src/components/content/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,11 @@

import {memo} from 'react';
import type * as Types from '@a2ui/web_core/types/types';
import {toMaterialSymbolLigature} from '@a2ui/web_core/styles/icons';
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();
}

/**
* Icon component - renders an icon using Material Symbols Outlined font.
*
Expand All @@ -51,7 +43,7 @@ export const Icon = memo(function Icon({node, surfaceId}: A2UIComponentProps<Typ
}

// Convert camelCase to snake_case for Material Symbols
const snakeCaseName = toSnakeCase(iconName);
const snakeCaseName = toMaterialSymbolLigature(iconName);

// Apply --weight CSS variable on root div (:host equivalent) for flex layouts
const hostStyle: React.CSSProperties =
Expand Down
3 changes: 3 additions & 0 deletions renderers/react/src/web-core-icons.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
declare module '@a2ui/web_core/styles/icons' {
export function toMaterialSymbolLigature(iconName: string): string;
}
3 changes: 2 additions & 1 deletion renderers/react/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"isolatedModules": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
"@/*": ["./src/*"],
"@a2ui/web_core/styles/icons": ["../web_core/src/v0_8/styles/icons.ts"]
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The path alias you've added here should be sufficient for TypeScript to resolve the module and its types directly from the source .ts file. The new declaration file renderers/react/src/web-core-icons.d.ts appears to be redundant and adds a maintenance burden, as it would need to be manually kept in sync with icons.ts.

Consider removing renderers/react/src/web-core-icons.d.ts and relying solely on the path mapping.

}
},
"include": ["src/**/*", "tests/**/*"],
Expand Down
18 changes: 15 additions & 3 deletions renderers/react/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,13 @@
* limitations under the License.
*/

import { fileURLToPath } from 'node:url';
import { defineConfig } from 'vitest/config';

const webCoreIconsPath = fileURLToPath(
new URL('../web_core/src/v0_8/styles/icons.ts', import.meta.url)
);

export default defineConfig({
test: {
globals: true,
Expand All @@ -30,8 +35,15 @@ export default defineConfig({
},
},
resolve: {
alias: {
'@': './src',
},
alias: [
{
find: '@a2ui/web_core/styles/icons',
replacement: webCoreIconsPath,
},
{
find: '@',
replacement: './src',
},
],
},
});
38 changes: 38 additions & 0 deletions renderers/web_core/src/v0_8/styles/icons.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import * as assert from 'node:assert';
import { describe, it } from 'node:test';
import { toMaterialSymbolLigature } from './icons.js';

describe('Icon styles helpers', () => {
it('leaves simple icon names unchanged', () => {
assert.strictEqual(toMaterialSymbolLigature('home'), 'home');
});

it('converts camelCase names to snake_case ligatures', () => {
assert.strictEqual(toMaterialSymbolLigature('shoppingCart'), 'shopping_cart');
assert.strictEqual(toMaterialSymbolLigature('accountCircle'), 'account_circle');
Comment on lines +27 to +28
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

It would be good to add test cases for icon names that start with a capital letter to ensure they are converted correctly without a leading underscore, which would align with the suggested change in icons.ts.

    assert.strictEqual(toMaterialSymbolLigature('shoppingCart'), 'shopping_cart');
    assert.strictEqual(toMaterialSymbolLigature('ShoppingCart'), 'shopping_cart');
    assert.strictEqual(toMaterialSymbolLigature('accountCircle'), 'account_circle');
    assert.strictEqual(toMaterialSymbolLigature('AccountCircle'), 'account_circle');

});

it('keeps existing separators and lowercases the result', () => {
assert.strictEqual(
toMaterialSymbolLigature('unknownIconName12345'),
'unknown_icon_name12345'
);
assert.strictEqual(toMaterialSymbolLigature('already_snake_case'), 'already_snake_case');
});
});
8 changes: 8 additions & 0 deletions renderers/web_core/src/v0_8/styles/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@
* limitations under the License.
*/

/**
* Convert an A2UI camelCase icon name into the snake_case ligature used by
* Material Symbols.
*/
export function toMaterialSymbolLigature(iconName: string): string {
return iconName.replace(/([A-Z])/g, '_$1').toLowerCase();
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The current implementation can produce a leading underscore if iconName starts with a capital letter (e.g., 'ShoppingCart' becomes '_shopping_cart'). Material Symbol ligatures don't typically start with an underscore.

You can fix this by removing the leading underscore after the replacement, before converting to lower case.

Suggested change
return iconName.replace(/([A-Z])/g, '_$1').toLowerCase();
return iconName.replace(/([A-Z])/g, '_$1').replace(/^_/, '').toLowerCase();

}

/**
* CSS classes for Google Symbols.
*
Expand Down