Skip to content

Commit 0c949b6

Browse files
authored
feat(a2ui): render real Material Symbols icons (not the name as text) (#720)
* docs: design for a2ui Material Symbols icon rendering * feat(a2ui): render Material Symbols glyphs in the Icon component (not the name as text) Aligns the a2ui catalog Icon with the canonical A2UI renderer: the icon name is a Material Symbols ligature rendered via the Material Symbols Outlined font (currentColor, size prop). The library injects no web font; the a2ui demos load the Material Symbols stylesheet in index.html and the README documents the requirement for consumers. Unknown/unloaded names fall back gracefully.
1 parent 5c89dd2 commit 0c949b6

7 files changed

Lines changed: 111 additions & 5 deletions

File tree

cockpit/ag-ui/a2ui/angular/src/index.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@
55
<title>AG-UI A2UI — Angular</title>
66
<base href="/" />
77
<meta name="viewport" content="width=device-width, initial-scale=1" />
8+
<!-- A2UI Icon component renders Material Symbols by ligature name. -->
9+
<link rel="preconnect" href="https://fonts.googleapis.com" />
10+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
11+
<link
12+
rel="stylesheet"
13+
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200"
14+
/>
815
</head>
916
<body>
1017
<app-a2ui></app-a2ui>

cockpit/chat/a2ui/angular/src/index.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@
55
<title>Chat A2UI — Angular</title>
66
<base href="/" />
77
<meta name="viewport" content="width=device-width, initial-scale=1" />
8+
<!-- A2UI Icon component renders Material Symbols by ligature name. -->
9+
<link rel="preconnect" href="https://fonts.googleapis.com" />
10+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
11+
<link
12+
rel="stylesheet"
13+
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200"
14+
/>
815
</head>
916
<body>
1017
<app-a2ui></app-a2ui>
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# a2ui Icon — render real Material Symbols (not the name as text) — Design
2+
3+
**Status:** Approved direction (2026-06-20, brainstormed).
4+
5+
## Problem
6+
The a2ui catalog `Icon` component (`libs/chat/src/lib/a2ui/catalog/icon.component.ts`) renders the icon **name as literal text** — a spec with `Icon { name: "trending_up" }` shows the string "trending_up" instead of a glyph.
7+
8+
## Alignment
9+
The canonical A2UI protocol/renderer uses **Material Symbols**: the Icon `name` is a Material Symbols identifier (`check`, `trending_up`, `star`, …), rendered via the Material Symbols font where the name is a *ligature*. Aligning the Angular renderer means doing the same — render the name through the Material Symbols Outlined font.
10+
11+
## Decisions (settled via brainstorm)
12+
- **Render Material Symbols** (protocol-aligned) — the component outputs the name into a `material-symbols-outlined`-classed span so the font renders the glyph. `currentColor` (theme-aware, matches the repo's inline-SVG convention); size via `font-size`.
13+
- **Demos load the font; document for consumers.** `@threadplane/chat` does **not** inject any CDN `<link>` at runtime. The a2ui demo apps load the Material Symbols Outlined stylesheet in their `index.html`; the lib README documents the requirement (standard for icon fonts).
14+
- **No backend prompt change.** The LLM keeps emitting Material Symbols names. Valid names render; unknown / not-yet-loaded names fall back gracefully (browser default) — acceptable per the "keep open, rely on fallback" choice.
15+
16+
## Changes
17+
18+
### 1. `libs/chat/src/lib/a2ui/catalog/icon.component.ts`
19+
- Add `material-symbols-outlined` to the span's class list (alongside `a2ui-icon`); keep rendering `{{ effectiveName() }}` (the ligature). The global Material Symbols stylesheet (loaded by the host) styles `.material-symbols-outlined`; the component's own `.a2ui-icon` style sets `color: currentColor`, line-height, and the `font-size` from `size()` (default 1.125rem). Render nothing when there is no name.
20+
- Keep all existing inputs (`name`/`icon` alias, `size`, framework inputs) unchanged.
21+
22+
### 2. Demo apps — load the font
23+
Add to the `<head>` of each a2ui demo's `index.html`:
24+
```html
25+
<link rel="preconnect" href="https://fonts.googleapis.com" />
26+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
27+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
28+
```
29+
Files: `examples/chat/angular/src/index.html`, `examples/ag-ui/angular/src/index.html`, `cockpit/chat/a2ui/angular/src/index.html`, `cockpit/ag-ui/a2ui/angular/src/index.html`.
30+
31+
### 3. Docs
32+
- `libs/chat/README.md` (and/or the a2ui getting-started doc): a short "Icons" note — the a2ui `Icon` component renders Material Symbols; include the Material Symbols Outlined stylesheet in your app's `<head>` for glyphs to render; names are Material Symbols identifiers.
33+
34+
### 4. Tests
35+
- `icon.component.spec.ts`: assert the rendered span carries the `material-symbols-outlined` class and the icon name as text content (the ligature), and applies `font-size` from `size`. (Glyph rendering itself needs the font + a browser; covered by the visual smoke.)
36+
37+
## Verification
38+
- `nx run-many -t test lint build --projects=chat` — green.
39+
- Build the 4 a2ui demo apps — green.
40+
- **Visual smoke**: serve one a2ui demo (e.g. cockpit/ag-ui/a2ui or examples/chat a2ui mode) and confirm a spec with icons renders **glyphs** (not the raw names); screenshot. (Live LLM optional — a static spec/fixture with an `Icon` suffices.)
41+
- PR + auto-merge + watcher.
42+
43+
## Risks
44+
- **FOUC / fallback:** before the font loads, the ligature text shows briefly; for an unknown name the browser shows tofu/nothing. Acceptable per the agreed fallback behavior; the `a2ui-icon` style can `overflow:hidden`/fixed-box to limit layout shift.
45+
- **No e2e text assertions on icon names** were found, so switching to glyphs won't break existing e2e.
46+
- Library stays CDN-free; only demos add the font link.

examples/ag-ui/angular/src/index.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@
55
<title>AG-UI Chat — Threadplane Example</title>
66
<base href="/" />
77
<meta name="viewport" content="width=device-width, initial-scale=1" />
8+
<!-- A2UI Icon component renders Material Symbols by ligature name. -->
9+
<link rel="preconnect" href="https://fonts.googleapis.com" />
10+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
11+
<link
12+
rel="stylesheet"
13+
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200"
14+
/>
815
<script>
916
// Pre-bootstrap color-scheme apply: read persisted palette and set
1017
// both `data-color-scheme` (drives the demo page bg/text) and

examples/chat/angular/src/index.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@
55
<title>Threadplane chat — canonical demo</title>
66
<base href="/" />
77
<meta name="viewport" content="width=device-width, initial-scale=1" />
8+
<!-- A2UI Icon component renders Material Symbols by ligature name. -->
9+
<link rel="preconnect" href="https://fonts.googleapis.com" />
10+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
11+
<link
12+
rel="stylesheet"
13+
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200"
14+
/>
815
<script>
916
// Pre-bootstrap color-scheme apply: read persisted palette and set
1017
// both `data-color-scheme` (drives the demo page bg/text) and

libs/chat/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,17 @@ Agents can emit surface specs via `buildA2uiActionMessage(...)`. Actions from ca
173173

174174
The built-in catalog ships via `a2uiBasicCatalog`. Compose a custom catalog with `withViews()` and pass it to the surface.
175175

176+
**Icons.** The catalog `Icon` component renders [Material Symbols](https://fonts.google.com/icons) by name (the A2UI canonical icon set — e.g. `check`, `trending_up`, `star`). For glyphs to render, include the Material Symbols Outlined stylesheet in your app's `<head>` (the library does not inject any web font):
177+
178+
```html
179+
<link
180+
rel="stylesheet"
181+
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200"
182+
/>
183+
```
184+
185+
Without the font, the icon name falls back to plain text. Icons inherit `currentColor` and size via the `size` prop.
186+
176187
### Streaming markdown
177188

178189
`<chat-streaming-md>` renders markdown token-by-token as the agent streams. The `cacheplaneMarkdownViews` registry maps each CommonMark node type to an Angular component.

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

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,35 @@ import type { Spec } from '@json-render/core';
66
selector: 'a2ui-icon',
77
standalone: true,
88
template: `
9-
<span
10-
class="a2ui-icon"
11-
[style.font-size]="size() ? size() + 'px' : '1.125rem'"
12-
[attr.aria-label]="effectiveName()"
13-
>{{ effectiveName() }}</span>
9+
@if (effectiveName(); as name) {
10+
<span
11+
class="a2ui-icon material-symbols-outlined"
12+
[style.font-size]="size() ? size() + 'px' : '1.125rem'"
13+
[attr.aria-label]="name"
14+
role="img"
15+
>{{ name }}</span>
16+
}
1417
`,
1518
styles: [`
19+
/* Renders Material Symbols by ligature name (A2UI's canonical icon set).
20+
Relies only on the Material Symbols Outlined @font-face being present —
21+
host apps load the stylesheet (see README). Unknown / not-yet-loaded
22+
names fall back to the browser default glyph. */
1623
.a2ui-icon {
24+
font-family: 'Material Symbols Outlined';
25+
font-weight: normal;
26+
font-style: normal;
27+
line-height: 1;
28+
letter-spacing: normal;
29+
text-transform: none;
30+
white-space: nowrap;
31+
word-wrap: normal;
32+
direction: ltr;
33+
font-feature-settings: 'liga';
34+
-webkit-font-feature-settings: 'liga';
35+
-webkit-font-smoothing: antialiased;
36+
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
37+
color: currentColor;
1738
display: inline-flex;
1839
align-items: center;
1940
justify-content: center;

0 commit comments

Comments
 (0)