Skip to content
Merged
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
79 changes: 76 additions & 3 deletions apps/website/content/docs/chat/api/api-docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@
"description": "",
"optional": false
},
{
"name": "description",
"type": "InputSignal<string>",
"description": "v1 canonical prop: short description / title rendered above the player.",
"optional": false
},
{
"name": "emit",
"type": "InputSignal<object>",
Expand Down Expand Up @@ -178,7 +184,7 @@
{
"name": "checked",
"type": "InputSignal<boolean>",
"description": "",
"description": "Pre-v1 alias retained for back-compat.",
"optional": false
},
{
Expand All @@ -187,6 +193,12 @@
"description": "",
"optional": false
},
{
"name": "effectiveValue",
"type": "Signal<boolean>",
"description": "",
"optional": false
},
{
"name": "emit",
"type": "InputSignal<object>",
Expand All @@ -210,6 +222,12 @@
"type": "InputSignal<Spec | undefined>",
"description": "",
"optional": false
},
{
"name": "value",
"type": "InputSignal<boolean | undefined>",
"description": "v1 canonical prop: boolean checked state.",
"optional": false
}
],
"methods": [
Expand Down Expand Up @@ -471,6 +489,12 @@
"description": "",
"optional": false
},
{
"name": "effectiveName",
"type": "Signal<string>",
"description": "",
"optional": false
},
{
"name": "emit",
"type": "InputSignal<object>",
Expand All @@ -480,7 +504,7 @@
{
"name": "icon",
"type": "InputSignal<string>",
"description": "v1 prop name: icon (resolved string, e.g. a Unicode symbol or ligature name).",
"description": "Pre-v1 alias retained for back-compat.",
"optional": false
},
{
Expand All @@ -489,6 +513,12 @@
"description": "",
"optional": false
},
{
"name": "name",
"type": "InputSignal<string | undefined>",
"description": "v1 canonical prop.",
"optional": false
},
{
"name": "size",
"type": "InputSignal<number | null>",
Expand Down Expand Up @@ -535,6 +565,12 @@
"description": "",
"optional": false
},
{
"name": "fit",
"type": "InputSignal<ImageFit | undefined>",
"description": "v1 prop: CSS object-fit equivalent.",
"optional": false
},
{
"name": "height",
"type": "InputSignal<number | null>",
Expand All @@ -559,14 +595,39 @@
"description": "",
"optional": false
},
{
"name": "usageHint",
"type": "InputSignal<ImageUsageHint | undefined>",
"description": "v1 prop: sizing preset.",
"optional": false
},
{
"name": "width",
"type": "InputSignal<number | null>",
"description": "",
"optional": false
}
],
"methods": []
"methods": [
{
"name": "explicitHeight",
"signature": "explicitHeight()",
"description": "",
"params": []
},
{
"name": "explicitWidth",
"signature": "explicitWidth()",
"description": "",
"params": []
},
{
"name": "hintStyle",
"signature": "hintStyle()",
"description": "",
"params": []
}
]
},
{
"name": "A2uiListComponent",
Expand All @@ -575,6 +636,18 @@
"params": [],
"examples": [],
"properties": [
{
"name": "alignment",
"type": "InputSignal<\"center\" | \"start\" | \"end\" | \"stretch\" | undefined>",
"description": "v1 canonical prop: cross-axis alignment.",
"optional": false
},
{
"name": "alignmentCss",
"type": "Signal<string | null>",
"description": "",
"optional": false
},
{
"name": "bindings",
"type": "InputSignal<Record<string, string>>",
Expand Down
10 changes: 10 additions & 0 deletions examples/chat/angular/src/app/modes/welcome-suggestions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,14 @@ export const WELCOME_SUGGESTIONS: readonly WelcomeSuggestion[] = [
value:
'Render a booking surface: a heading "Book your trip", a DateTimeInput for travel date, a horizontal divider, then a Row containing two Cards (one for departure city, one for return city) each with a TextField. Below the Row add a primary "Continue" Button whose action opens a Modal containing a confirmation Column with a summary Text and Confirm / Cancel Buttons.',
},
{
label: 'Smoke: media + layout kitchen sink',
value:
'Render a Card containing a Tabs component with two tabs labeled "Media" and "Layout". Under the Media tab show a Column containing: a header Image (use https://placehold.co/600x300/4f8df5/ffffff.png as the URL), an Icon (any icon name from the canonical set, e.g. star), a short Text caption, an AudioPlayer (use https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3 as the URL), and a Video (use https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4 as the URL). Under the Layout tab show: a Row containing two Text components separated by a vertical Divider, then a horizontal Divider, then a List of three Text bullet items, then a Column containing two Text components.',
},
{
label: 'Smoke: interactive form kitchen sink',
value:
'Render a Card titled "Profile setup" containing a Column with: a TextField for display name, a Slider for "experience years" (range 0-30), a CheckBox for "subscribe to newsletter", a DateTimeInput for birthday (date only), a MultipleChoice for "favorite frameworks" with options Angular, React, Vue, Svelte and maxAllowedSelections of 3 (multi-select), a horizontal Divider, a Row containing a primary "Save" Button and a secondary "Open details" Button whose action opens a Modal with a Column containing a Text summary and a Close Button.',
},
];
28 changes: 22 additions & 6 deletions libs/chat/src/lib/a2ui/catalog/audio-player.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,28 @@ import type { Spec } from '@json-render/core';
selector: 'a2ui-audio-player',
standalone: true,
template: `
<audio
class="a2ui-audio"
[src]="url()"
[autoplay]="autoPlay()"
[controls]="controls()"
></audio>
<div class="a2ui-audio-wrap">
@if (description()) {
<span class="a2ui-audio-description">{{ description() }}</span>
}
<audio
class="a2ui-audio"
[src]="url()"
[autoplay]="autoPlay()"
[controls]="controls()"
></audio>
</div>
`,
styles: [`
.a2ui-audio-wrap {
display: flex;
flex-direction: column;
gap: var(--a2ui-spacing-1);
}
.a2ui-audio-description {
font-size: var(--a2ui-typography-caption-size);
color: var(--a2ui-on-surface-variant);
}
.a2ui-audio {
display: block;
width: 100%;
Expand All @@ -22,6 +36,8 @@ import type { Spec } from '@json-render/core';
})
export class A2uiAudioPlayerComponent {
readonly url = input<string>('');
/** v1 canonical prop: short description / title rendered above the player. */
readonly description = input<string>('');
/** v1 prop name: autoPlay (camelCase). */
readonly autoPlay = input<boolean>(false);
readonly controls = input<boolean>(true);
Expand Down
18 changes: 15 additions & 3 deletions libs/chat/src/lib/a2ui/catalog/check-box.component.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
import { Component, input, ChangeDetectionStrategy } from '@angular/core';
import { Component, computed, input, ChangeDetectionStrategy } from '@angular/core';
import type { Spec } from '@json-render/core';
import { emitBinding } from './emit-binding';

Expand All @@ -9,7 +9,7 @@ import { emitBinding } from './emit-binding';
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<label class="a2ui-cb">
<input type="checkbox" [checked]="checked()" (change)="onChange($event)" class="a2ui-cb__input" />
<input type="checkbox" [checked]="effectiveValue()" (change)="onChange($event)" class="a2ui-cb__input" />
{{ label() }}
</label>
`,
Expand All @@ -32,6 +32,9 @@ import { emitBinding } from './emit-binding';
})
export class A2uiCheckBoxComponent {
readonly label = input<string>('');
/** v1 canonical prop: boolean checked state. */
readonly value = input<boolean | undefined>(undefined);
/** Pre-v1 alias retained for back-compat. */
readonly checked = input<boolean>(false);
readonly _bindings = input<Record<string, string>>({});
readonly emit = input<(event: string) => void>(() => { /* noop */ });
Expand All @@ -41,8 +44,17 @@ export class A2uiCheckBoxComponent {
readonly childKeys = input<string[]>([]);
readonly spec = input<Spec | undefined>(undefined);

protected readonly effectiveValue = computed(() => this.value() ?? this.checked());

onChange(event: Event): void {
const val = (event.target as HTMLInputElement).checked;
emitBinding(this.emit(), this._bindings(), 'checked', val);
// Emit on whichever binding the surface declared. v1 surfaces use
// `value`; pre-v1 used `checked`.
const bound = this._bindings();
if (bound['value']) {
emitBinding(this.emit(), bound, 'value', val);
} else {
emitBinding(this.emit(), bound, 'checked', val);
}
}
}
11 changes: 8 additions & 3 deletions libs/chat/src/lib/a2ui/catalog/icon.component.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
import { Component, input } from '@angular/core';
import { Component, computed, input } from '@angular/core';
import type { Spec } from '@json-render/core';

@Component({
Expand All @@ -9,7 +9,8 @@ import type { Spec } from '@json-render/core';
<span
class="a2ui-icon"
[style.font-size]="size() ? size() + 'px' : '1.125rem'"
>{{ icon() }}</span>
[attr.aria-label]="effectiveName()"
>{{ effectiveName() }}</span>
`,
styles: [`
.a2ui-icon {
Expand All @@ -21,7 +22,9 @@ import type { Spec } from '@json-render/core';
`],
})
export class A2uiIconComponent {
/** v1 prop name: icon (resolved string, e.g. a Unicode symbol or ligature name). */
/** v1 canonical prop. */
readonly name = input<string | undefined>(undefined);
/** Pre-v1 alias retained for back-compat. */
readonly icon = input<string>('');
readonly size = input<number | null>(null);
// Framework inputs required by the render harness.
Expand All @@ -30,4 +33,6 @@ export class A2uiIconComponent {
readonly loading = input<boolean>(false);
readonly childKeys = input<string[]>([]);
readonly spec = input<Spec | undefined>(undefined);

protected readonly effectiveName = computed(() => this.name() ?? this.icon());
}
40 changes: 38 additions & 2 deletions libs/chat/src/lib/a2ui/catalog/image.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,23 @@
import { Component, input } from '@angular/core';
import type { Spec } from '@json-render/core';

/** v1 fit values mapped 1:1 to CSS object-fit. */
type ImageFit = 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';

/** v1 usageHint maps to a sizing preset. The component renders fluid by
* default; usageHint sets a max-width / aspect-ratio to match common
* intents. */
type ImageUsageHint = 'icon' | 'avatar' | 'smallFeature' | 'mediumFeature' | 'largeFeature' | 'header';

const USAGE_HINT_STYLE: Record<ImageUsageHint, { maxWidth: string; aspectRatio?: string; borderRadius?: string }> = {
icon: { maxWidth: '24px', aspectRatio: '1 / 1' },
avatar: { maxWidth: '48px', aspectRatio: '1 / 1', borderRadius: '50%' },
smallFeature: { maxWidth: '160px' },
mediumFeature: { maxWidth: '320px' },
largeFeature: { maxWidth: '480px' },
header: { maxWidth: '100%', aspectRatio: '16 / 5' },
};

@Component({
selector: 'a2ui-image',
standalone: true,
Expand All @@ -10,8 +27,12 @@ import type { Spec } from '@json-render/core';
class="a2ui-img"
[src]="url()"
[alt]="alt()"
[style.width]="width() ? width() + 'px' : null"
[style.height]="height() ? height() + 'px' : null"
[style.width]="explicitWidth()"
[style.height]="explicitHeight()"
[style.object-fit]="fit()"
[style.max-width]="hintStyle()?.maxWidth"
[style.aspect-ratio]="hintStyle()?.aspectRatio || null"
[style.border-radius]="hintStyle()?.borderRadius || null"
/>
`,
styles: [`
Expand All @@ -27,10 +48,25 @@ export class A2uiImageComponent {
readonly alt = input<string>('');
readonly width = input<number | null>(null);
readonly height = input<number | null>(null);
/** v1 prop: CSS object-fit equivalent. */
readonly fit = input<ImageFit | undefined>(undefined);
/** v1 prop: sizing preset. */
readonly usageHint = input<ImageUsageHint | undefined>(undefined);
// Framework inputs required by the render harness.
readonly bindings = input<Record<string, string>>({});
readonly emit = input<(event: string) => void>(() => { /* noop */ });
readonly loading = input<boolean>(false);
readonly childKeys = input<string[]>([]);
readonly spec = input<Spec | undefined>(undefined);

protected explicitWidth(): string | null {
return this.width() != null ? this.width() + 'px' : null;
}
protected explicitHeight(): string | null {
return this.height() != null ? this.height() + 'px' : null;
}
protected hintStyle(): { maxWidth: string; aspectRatio?: string; borderRadius?: string } | null {
const h = this.usageHint();
return h ? USAGE_HINT_STYLE[h] : null;
}
}
12 changes: 11 additions & 1 deletion libs/chat/src/lib/a2ui/catalog/list.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { RenderElementComponent } from '@ngaf/render';
standalone: true,
imports: [RenderElementComponent],
template: `
<div [class]="listClass()">
<div [class]="listClass()" [style.align-items]="alignmentCss()">
@for (key of childKeys(); track key) {
<render-element [elementKey]="key" [spec]="spec()" />
}
Expand All @@ -34,6 +34,8 @@ export class A2uiListComponent {
readonly childKeys = input<string[]>([]);
readonly spec = input.required<Spec>();
readonly direction = input<'vertical' | 'horizontal'>('vertical');
/** v1 canonical prop: cross-axis alignment. */
readonly alignment = input<'start' | 'center' | 'end' | 'stretch' | undefined>(undefined);
// Framework inputs required by the render harness.
readonly bindings = input<Record<string, string>>({});
readonly emit = input<(event: string) => void>(() => { /* noop */ });
Expand All @@ -44,4 +46,12 @@ export class A2uiListComponent {
? 'a2ui-list--horizontal'
: 'a2ui-list--vertical';
});

protected readonly alignmentCss = computed<string | null>(() => {
const a = this.alignment();
if (!a) return null;
return a === 'start' ? 'flex-start'
: a === 'end' ? 'flex-end'
: a; // center / stretch are valid CSS values as-is
});
}
Loading
Loading