feat(macos): add Canvas A2UI renderer
This commit is contained in:
50
vendor/a2ui/renderers/angular/src/lib/catalog/audio.ts
vendored
Normal file
50
vendor/a2ui/renderers/angular/src/lib/catalog/audio.ts
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
Copyright 2025 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 { Component, computed, input } from '@angular/core';
|
||||
import { DynamicComponent } from '../rendering/dynamic-component';
|
||||
import { Primitives } from '@a2ui/lit/0.8';
|
||||
|
||||
@Component({
|
||||
selector: 'a2ui-audio',
|
||||
template: `
|
||||
@let resolvedUrl = this.resolvedUrl();
|
||||
|
||||
@if (resolvedUrl) {
|
||||
<section [class]="theme.components.AudioPlayer" [style]="theme.additionalStyles?.AudioPlayer">
|
||||
<audio controls [src]="resolvedUrl"></audio>
|
||||
</section>
|
||||
}
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
display: block;
|
||||
flex: var(--weight);
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
audio {
|
||||
display: block;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
`
|
||||
})
|
||||
export class Audio extends DynamicComponent {
|
||||
readonly url = input.required<Primitives.StringValue | null>();
|
||||
protected readonly resolvedUrl = computed(() => this.resolvePrimitive(this.url()));
|
||||
}
|
||||
56
vendor/a2ui/renderers/angular/src/lib/catalog/button.ts
vendored
Normal file
56
vendor/a2ui/renderers/angular/src/lib/catalog/button.ts
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
Copyright 2025 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 { Component, input } from '@angular/core';
|
||||
import { Types } from '@a2ui/lit/0.8';
|
||||
import { DynamicComponent } from '../rendering/dynamic-component';
|
||||
import { Renderer } from '../rendering/renderer';
|
||||
|
||||
@Component({
|
||||
selector: 'a2ui-button',
|
||||
imports: [Renderer],
|
||||
template: `
|
||||
<button
|
||||
[class]="theme.components.Button"
|
||||
[style]="theme.additionalStyles?.Button"
|
||||
(click)="handleClick()"
|
||||
>
|
||||
<ng-container
|
||||
a2ui-renderer
|
||||
[surfaceId]="surfaceId()!"
|
||||
[component]="component().properties.child"
|
||||
/>
|
||||
</button>
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
display: block;
|
||||
flex: var(--weight);
|
||||
min-height: 0;
|
||||
}
|
||||
`,
|
||||
})
|
||||
export class Button extends DynamicComponent<Types.ButtonNode> {
|
||||
readonly action = input.required<Types.Action | null>();
|
||||
|
||||
protected handleClick() {
|
||||
const action = this.action();
|
||||
|
||||
if (action) {
|
||||
super.sendAction(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
57
vendor/a2ui/renderers/angular/src/lib/catalog/card.ts
vendored
Normal file
57
vendor/a2ui/renderers/angular/src/lib/catalog/card.ts
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
Copyright 2025 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 { Component, ViewEncapsulation } from '@angular/core';
|
||||
import { DynamicComponent } from '../rendering/dynamic-component';
|
||||
import { Renderer } from '../rendering/renderer';
|
||||
import { Types } from '@a2ui/lit/0.8';
|
||||
|
||||
@Component({
|
||||
selector: 'a2ui-card',
|
||||
imports: [Renderer],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
styles: `
|
||||
a2ui-card {
|
||||
display: block;
|
||||
flex: var(--weight);
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
a2ui-card > section {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
a2ui-card > section > * {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
template: `
|
||||
@let properties = component().properties;
|
||||
@let children = properties.children || [properties.child];
|
||||
|
||||
<section [class]="theme.components.Card" [style]="theme.additionalStyles?.Card">
|
||||
@for (child of children; track child) {
|
||||
<ng-container a2ui-renderer [surfaceId]="surfaceId()!" [component]="child" />
|
||||
}
|
||||
</section>
|
||||
`,
|
||||
})
|
||||
export class Card extends DynamicComponent<Types.CardNode> { }
|
||||
73
vendor/a2ui/renderers/angular/src/lib/catalog/checkbox.ts
vendored
Normal file
73
vendor/a2ui/renderers/angular/src/lib/catalog/checkbox.ts
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
Copyright 2025 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 { Component, computed, input } from '@angular/core';
|
||||
import { DynamicComponent } from '../rendering/dynamic-component';
|
||||
import { Primitives } from '@a2ui/lit/0.8';
|
||||
|
||||
@Component({
|
||||
selector: 'a2ui-checkbox',
|
||||
template: `
|
||||
<section
|
||||
[class]="theme.components.CheckBox.container"
|
||||
[style]="theme.additionalStyles?.CheckBox"
|
||||
>
|
||||
<input
|
||||
autocomplete="off"
|
||||
type="checkbox"
|
||||
[id]="inputId"
|
||||
[checked]="inputChecked()"
|
||||
[class]="theme.components.CheckBox.element"
|
||||
(change)="handleChange($event)"
|
||||
/>
|
||||
|
||||
<label [htmlFor]="inputId" [class]="theme.components.CheckBox.label">{{
|
||||
resolvedLabel()
|
||||
}}</label>
|
||||
</section>
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
display: block;
|
||||
flex: var(--weight);
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
})
|
||||
export class Checkbox extends DynamicComponent {
|
||||
readonly value = input.required<Primitives.BooleanValue | null>();
|
||||
readonly label = input.required<Primitives.StringValue | null>();
|
||||
|
||||
protected inputChecked = computed(() => super.resolvePrimitive(this.value()) ?? false);
|
||||
protected resolvedLabel = computed(() => super.resolvePrimitive(this.label()));
|
||||
protected inputId = super.getUniqueId('a2ui-checkbox');
|
||||
|
||||
protected handleChange(event: Event) {
|
||||
const path = this.value()?.path;
|
||||
|
||||
if (!(event.target instanceof HTMLInputElement) || !path) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.processor.setData(this.component(), path, event.target.checked, this.surfaceId());
|
||||
}
|
||||
}
|
||||
96
vendor/a2ui/renderers/angular/src/lib/catalog/column.ts
vendored
Normal file
96
vendor/a2ui/renderers/angular/src/lib/catalog/column.ts
vendored
Normal file
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
Copyright 2025 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 { Component, computed, input } from '@angular/core';
|
||||
import { Types } from '@a2ui/lit/0.8';
|
||||
import { DynamicComponent } from '../rendering/dynamic-component';
|
||||
import { Renderer } from '../rendering/renderer';
|
||||
|
||||
@Component({
|
||||
selector: 'a2ui-column',
|
||||
imports: [Renderer],
|
||||
styles: `
|
||||
:host {
|
||||
display: flex;
|
||||
flex: var(--weight);
|
||||
}
|
||||
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.align-start {
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.align-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.align-end {
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.align-stretch {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.distribute-start {
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
.distribute-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.distribute-end {
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
.distribute-spaceBetween {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.distribute-spaceAround {
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.distribute-spaceEvenly {
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
`,
|
||||
template: `
|
||||
<section [class]="classes()" [style]="theme.additionalStyles?.Column">
|
||||
@for (child of component().properties.children; track child) {
|
||||
<ng-container a2ui-renderer [surfaceId]="surfaceId()!" [component]="child" />
|
||||
}
|
||||
</section>
|
||||
`,
|
||||
})
|
||||
export class Column extends DynamicComponent<Types.ColumnNode> {
|
||||
readonly alignment = input<Types.ResolvedColumn['alignment']>('stretch');
|
||||
readonly distribution = input<Types.ResolvedColumn['distribution']>('start');
|
||||
|
||||
protected readonly classes = computed(() => ({
|
||||
...this.theme.components.Column,
|
||||
[`align-${this.alignment()}`]: true,
|
||||
[`distribute-${this.distribution()}`]: true,
|
||||
}));
|
||||
}
|
||||
127
vendor/a2ui/renderers/angular/src/lib/catalog/datetime-input.ts
vendored
Normal file
127
vendor/a2ui/renderers/angular/src/lib/catalog/datetime-input.ts
vendored
Normal file
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
Copyright 2025 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 { computed, Component, input } from '@angular/core';
|
||||
import { DynamicComponent } from '../rendering/dynamic-component';
|
||||
import { Primitives } from '@a2ui/lit/0.8';
|
||||
|
||||
@Component({
|
||||
selector: 'a2ui-datetime-input',
|
||||
template: `
|
||||
<section [class]="theme.components.DateTimeInput.container">
|
||||
<label [for]="inputId" [class]="theme.components.DateTimeInput.label">{{ label() }}</label>
|
||||
|
||||
<input
|
||||
autocomplete="off"
|
||||
[attr.type]="inputType()"
|
||||
[id]="inputId"
|
||||
[class]="theme.components.DateTimeInput.element"
|
||||
[style]="theme.additionalStyles?.DateTimeInput"
|
||||
[value]="inputValue()"
|
||||
(input)="handleInput($event)"
|
||||
/>
|
||||
</section>
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
display: block;
|
||||
flex: var(--weight);
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
`,
|
||||
})
|
||||
export class DatetimeInput extends DynamicComponent {
|
||||
readonly value = input.required<Primitives.StringValue | null>();
|
||||
readonly enableDate = input.required<boolean>();
|
||||
readonly enableTime = input.required<boolean>();
|
||||
protected readonly inputId = super.getUniqueId('a2ui-datetime-input');
|
||||
|
||||
protected inputType = computed(() => {
|
||||
const enableDate = this.enableDate();
|
||||
const enableTime = this.enableTime();
|
||||
|
||||
if (enableDate && enableTime) {
|
||||
return 'datetime-local';
|
||||
} else if (enableDate) {
|
||||
return 'date';
|
||||
} else if (enableTime) {
|
||||
return 'time';
|
||||
}
|
||||
|
||||
return 'datetime-local';
|
||||
});
|
||||
|
||||
protected label = computed(() => {
|
||||
// TODO: this should likely be passed from the model.
|
||||
const inputType = this.inputType();
|
||||
|
||||
if (inputType === 'date') {
|
||||
return 'Date';
|
||||
} else if (inputType === 'time') {
|
||||
return 'Time';
|
||||
}
|
||||
|
||||
return 'Date & Time';
|
||||
});
|
||||
|
||||
protected inputValue = computed(() => {
|
||||
const inputType = this.inputType();
|
||||
const parsed = super.resolvePrimitive(this.value()) || '';
|
||||
const date = parsed ? new Date(parsed) : null;
|
||||
|
||||
if (!date || isNaN(date.getTime())) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const year = this.padNumber(date.getFullYear());
|
||||
const month = this.padNumber(date.getMonth());
|
||||
const day = this.padNumber(date.getDate());
|
||||
const hours = this.padNumber(date.getHours());
|
||||
const minutes = this.padNumber(date.getMinutes());
|
||||
|
||||
// Browsers are picky with what format they allow for the `value` attribute of date/time inputs.
|
||||
// We need to parse it out of the provided value. Note that we don't use `toISOString`,
|
||||
// because the resulting value is relative to UTC.
|
||||
if (inputType === 'date') {
|
||||
return `${year}-${month}-${day}`;
|
||||
} else if (inputType === 'time') {
|
||||
return `${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
||||
});
|
||||
|
||||
protected handleInput(event: Event) {
|
||||
const path = this.value()?.path;
|
||||
|
||||
if (!(event.target instanceof HTMLInputElement) || !path) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.processor.setData(this.component(), path, event.target.value, this.surfaceId());
|
||||
}
|
||||
|
||||
private padNumber(value: number) {
|
||||
return value.toString().padStart(2, '0');
|
||||
}
|
||||
}
|
||||
185
vendor/a2ui/renderers/angular/src/lib/catalog/default.ts
vendored
Normal file
185
vendor/a2ui/renderers/angular/src/lib/catalog/default.ts
vendored
Normal file
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
Copyright 2025 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 { inputBinding } from '@angular/core';
|
||||
import { Types } from '@a2ui/lit/0.8';
|
||||
import { Catalog } from '../rendering/catalog';
|
||||
import { Row } from './row';
|
||||
import { Column } from './column';
|
||||
import { Text } from './text';
|
||||
|
||||
export const DEFAULT_CATALOG: Catalog = {
|
||||
Row: {
|
||||
type: () => Row,
|
||||
bindings: (node) => {
|
||||
const properties = (node as Types.RowNode).properties;
|
||||
return [
|
||||
inputBinding('alignment', () => properties.alignment ?? 'stretch'),
|
||||
inputBinding('distribution', () => properties.distribution ?? 'start'),
|
||||
];
|
||||
},
|
||||
},
|
||||
|
||||
Column: {
|
||||
type: () => Column,
|
||||
bindings: (node) => {
|
||||
const properties = (node as Types.ColumnNode).properties;
|
||||
return [
|
||||
inputBinding('alignment', () => properties.alignment ?? 'stretch'),
|
||||
inputBinding('distribution', () => properties.distribution ?? 'start'),
|
||||
];
|
||||
},
|
||||
},
|
||||
|
||||
List: {
|
||||
type: () => import('./list').then((r) => r.List),
|
||||
bindings: (node) => {
|
||||
const properties = (node as Types.ListNode).properties;
|
||||
return [inputBinding('direction', () => properties.direction ?? 'vertical')];
|
||||
},
|
||||
},
|
||||
|
||||
Card: () => import('./card').then((r) => r.Card),
|
||||
|
||||
Image: {
|
||||
type: () => import('./image').then((r) => r.Image),
|
||||
bindings: (node) => {
|
||||
const properties = (node as Types.ImageNode).properties;
|
||||
return [
|
||||
inputBinding('url', () => properties.url),
|
||||
inputBinding('usageHint', () => properties.usageHint),
|
||||
];
|
||||
},
|
||||
},
|
||||
|
||||
Icon: {
|
||||
type: () => import('./icon').then((r) => r.Icon),
|
||||
bindings: (node) => {
|
||||
const properties = (node as Types.IconNode).properties;
|
||||
return [inputBinding('name', () => properties.name)];
|
||||
},
|
||||
},
|
||||
|
||||
Video: {
|
||||
type: () => import('./video').then((r) => r.Video),
|
||||
bindings: (node) => {
|
||||
const properties = (node as Types.VideoNode).properties;
|
||||
return [inputBinding('url', () => properties.url)];
|
||||
},
|
||||
},
|
||||
|
||||
AudioPlayer: {
|
||||
type: () => import('./audio').then((r) => r.Audio),
|
||||
bindings: (node) => {
|
||||
const properties = (node as Types.AudioPlayerNode).properties;
|
||||
return [inputBinding('url', () => properties.url)];
|
||||
},
|
||||
},
|
||||
|
||||
Text: {
|
||||
type: () => Text,
|
||||
bindings: (node) => {
|
||||
const properties = (node as Types.TextNode).properties;
|
||||
return [
|
||||
inputBinding('text', () => properties.text),
|
||||
inputBinding('usageHint', () => properties.usageHint || null),
|
||||
];
|
||||
},
|
||||
},
|
||||
|
||||
Button: {
|
||||
type: () => import('./button').then((r) => r.Button),
|
||||
bindings: (node) => {
|
||||
const properties = (node as Types.ButtonNode).properties;
|
||||
return [inputBinding('action', () => properties.action)];
|
||||
},
|
||||
},
|
||||
|
||||
Divider: () => import('./divider').then((r) => r.Divider),
|
||||
|
||||
MultipleChoice: {
|
||||
type: () => import('./multiple-choice').then((r) => r.MultipleChoice),
|
||||
bindings: (node) => {
|
||||
const properties = (node as Types.MultipleChoiceNode).properties;
|
||||
return [
|
||||
inputBinding('options', () => properties.options || []),
|
||||
inputBinding('value', () => properties.selections),
|
||||
inputBinding('description', () => 'Select an item'), // TODO: this should be defined in the properties
|
||||
];
|
||||
},
|
||||
},
|
||||
|
||||
TextField: {
|
||||
type: () => import('./text-field').then((r) => r.TextField),
|
||||
bindings: (node) => {
|
||||
const properties = (node as Types.TextFieldNode).properties;
|
||||
return [
|
||||
inputBinding('text', () => properties.text ?? null),
|
||||
inputBinding('label', () => properties.label),
|
||||
inputBinding('inputType', () => properties.type),
|
||||
];
|
||||
},
|
||||
},
|
||||
|
||||
DateTimeInput: {
|
||||
type: () => import('./datetime-input').then((r) => r.DatetimeInput),
|
||||
bindings: (node) => {
|
||||
const properties = (node as Types.DateTimeInputNode).properties;
|
||||
return [
|
||||
inputBinding('enableDate', () => properties.enableDate),
|
||||
inputBinding('enableTime', () => properties.enableTime),
|
||||
inputBinding('value', () => properties.value),
|
||||
];
|
||||
},
|
||||
},
|
||||
|
||||
CheckBox: {
|
||||
type: () => import('./checkbox').then((r) => r.Checkbox),
|
||||
bindings: (node) => {
|
||||
const properties = (node as Types.CheckboxNode).properties;
|
||||
return [
|
||||
inputBinding('label', () => properties.label),
|
||||
inputBinding('value', () => properties.value),
|
||||
];
|
||||
},
|
||||
},
|
||||
|
||||
Slider: {
|
||||
type: () => import('./slider').then((r) => r.Slider),
|
||||
bindings: (node) => {
|
||||
const properties = (node as Types.SliderNode).properties;
|
||||
return [
|
||||
inputBinding('value', () => properties.value),
|
||||
inputBinding('minValue', () => properties.minValue),
|
||||
inputBinding('maxValue', () => properties.maxValue),
|
||||
inputBinding('label', () => ''), // TODO: this should be defined in the properties
|
||||
];
|
||||
},
|
||||
},
|
||||
|
||||
Tabs: {
|
||||
type: () => import('./tabs').then((r) => r.Tabs),
|
||||
bindings: (node) => {
|
||||
const properties = (node as Types.TabsNode).properties;
|
||||
return [inputBinding('tabs', () => properties.tabItems)];
|
||||
},
|
||||
},
|
||||
|
||||
Modal: {
|
||||
type: () => import('./modal').then((r) => r.Modal),
|
||||
bindings: () => [],
|
||||
},
|
||||
};
|
||||
37
vendor/a2ui/renderers/angular/src/lib/catalog/divider.ts
vendored
Normal file
37
vendor/a2ui/renderers/angular/src/lib/catalog/divider.ts
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
Copyright 2025 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 { Component } from '@angular/core';
|
||||
import { DynamicComponent } from '../rendering/dynamic-component';
|
||||
|
||||
@Component({
|
||||
selector: 'a2ui-divider',
|
||||
template: '<hr [class]="theme.components.Divider" [style]="theme.additionalStyles?.Divider"/>',
|
||||
styles: `
|
||||
:host {
|
||||
display: block;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
hr {
|
||||
height: 1px;
|
||||
background: #ccc;
|
||||
border: none;
|
||||
}
|
||||
`,
|
||||
})
|
||||
export class Divider extends DynamicComponent {}
|
||||
44
vendor/a2ui/renderers/angular/src/lib/catalog/icon.ts
vendored
Normal file
44
vendor/a2ui/renderers/angular/src/lib/catalog/icon.ts
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
Copyright 2025 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 { Component, computed, input } from '@angular/core';
|
||||
import { DynamicComponent } from '../rendering/dynamic-component';
|
||||
import { Primitives } from '@a2ui/lit/0.8';
|
||||
|
||||
@Component({
|
||||
selector: 'a2ui-icon',
|
||||
styles: `
|
||||
:host {
|
||||
display: block;
|
||||
flex: var(--weight);
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
`,
|
||||
template: `
|
||||
@let resolvedName = this.resolvedName();
|
||||
|
||||
@if (resolvedName) {
|
||||
<section [class]="theme.components.Icon" [style]="theme.additionalStyles?.Icon">
|
||||
<span class="g-icon">{{ resolvedName }}</span>
|
||||
</section>
|
||||
}
|
||||
`,
|
||||
})
|
||||
export class Icon extends DynamicComponent {
|
||||
readonly name = input.required<Primitives.StringValue | null>();
|
||||
protected readonly resolvedName = computed(() => this.resolvePrimitive(this.name()));
|
||||
}
|
||||
62
vendor/a2ui/renderers/angular/src/lib/catalog/image.ts
vendored
Normal file
62
vendor/a2ui/renderers/angular/src/lib/catalog/image.ts
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
Copyright 2025 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 { Component, computed, input } from '@angular/core';
|
||||
import { Primitives, Styles, Types } from '@a2ui/lit/0.8';
|
||||
import { DynamicComponent } from '../rendering/dynamic-component';
|
||||
|
||||
@Component({
|
||||
selector: 'a2ui-image',
|
||||
styles: `
|
||||
:host {
|
||||
display: block;
|
||||
flex: var(--weight);
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
`,
|
||||
template: `
|
||||
@let resolvedUrl = this.resolvedUrl();
|
||||
|
||||
@if (resolvedUrl) {
|
||||
<section [class]="classes()" [style]="theme.additionalStyles?.Image">
|
||||
<img [src]="resolvedUrl" />
|
||||
</section>
|
||||
}
|
||||
`,
|
||||
})
|
||||
export class Image extends DynamicComponent {
|
||||
readonly url = input.required<Primitives.StringValue | null>();
|
||||
readonly usageHint = input.required<Types.ResolvedImage['usageHint'] | null>();
|
||||
|
||||
protected readonly resolvedUrl = computed(() => this.resolvePrimitive(this.url()));
|
||||
|
||||
protected classes = computed(() => {
|
||||
const usageHint = this.usageHint();
|
||||
|
||||
return Styles.merge(
|
||||
this.theme.components.Image.all,
|
||||
usageHint ? this.theme.components.Image[usageHint] : {},
|
||||
);
|
||||
});
|
||||
}
|
||||
63
vendor/a2ui/renderers/angular/src/lib/catalog/list.ts
vendored
Normal file
63
vendor/a2ui/renderers/angular/src/lib/catalog/list.ts
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
Copyright 2025 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 { Component, input } from '@angular/core';
|
||||
import { Types } from '@a2ui/lit/0.8';
|
||||
import { DynamicComponent } from '../rendering/dynamic-component';
|
||||
import { Renderer } from '../rendering/renderer';
|
||||
|
||||
@Component({
|
||||
selector: 'a2ui-list',
|
||||
imports: [Renderer],
|
||||
host: {
|
||||
'[attr.direction]': 'direction()',
|
||||
},
|
||||
styles: `
|
||||
:host {
|
||||
display: block;
|
||||
flex: var(--weight);
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
:host([direction='vertical']) section {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
:host([direction='horizontal']) section {
|
||||
display: flex;
|
||||
max-width: 100%;
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
scrollbar-width: none;
|
||||
|
||||
> ::slotted(*) {
|
||||
flex: 1 0 fit-content;
|
||||
max-width: min(80%, 400px);
|
||||
}
|
||||
}
|
||||
`,
|
||||
template: `
|
||||
<section [class]="theme.components.List" [style]="theme.additionalStyles?.List">
|
||||
@for (child of component().properties.children; track child) {
|
||||
<ng-container a2ui-renderer [surfaceId]="surfaceId()!" [component]="child" />
|
||||
}
|
||||
</section>
|
||||
`,
|
||||
})
|
||||
export class List extends DynamicComponent<Types.ListNode> {
|
||||
readonly direction = input<'vertical' | 'horizontal'>('vertical');
|
||||
}
|
||||
113
vendor/a2ui/renderers/angular/src/lib/catalog/modal.ts
vendored
Normal file
113
vendor/a2ui/renderers/angular/src/lib/catalog/modal.ts
vendored
Normal file
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
Copyright 2025 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 { Component, signal, viewChild, ElementRef, effect } from '@angular/core';
|
||||
import { DynamicComponent } from '../rendering/dynamic-component';
|
||||
import { Types } from '@a2ui/lit/0.8';
|
||||
import { Renderer } from '../rendering';
|
||||
|
||||
@Component({
|
||||
selector: 'a2ui-modal',
|
||||
imports: [Renderer],
|
||||
template: `
|
||||
@if (showDialog()) {
|
||||
<dialog #dialog [class]="theme.components.Modal.backdrop" (click)="handleDialogClick($event)">
|
||||
<section [class]="theme.components.Modal.element" [style]="theme.additionalStyles?.Modal">
|
||||
<div class="controls">
|
||||
<button (click)="closeDialog()">
|
||||
<span class="g-icon">close</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ng-container
|
||||
a2ui-renderer
|
||||
[surfaceId]="surfaceId()!"
|
||||
[component]="component().properties.contentChild"
|
||||
/>
|
||||
</section>
|
||||
</dialog>
|
||||
} @else {
|
||||
<section (click)="showDialog.set(true)">
|
||||
<ng-container
|
||||
a2ui-renderer
|
||||
[surfaceId]="surfaceId()!"
|
||||
[component]="component().properties.entryPointChild"
|
||||
/>
|
||||
</section>
|
||||
}
|
||||
`,
|
||||
styles: `
|
||||
dialog {
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
|
||||
& section {
|
||||
& .controls {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
margin-bottom: 4px;
|
||||
|
||||
& button {
|
||||
padding: 0;
|
||||
background: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
pointer: cursor;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
export class Modal extends DynamicComponent<Types.ModalNode> {
|
||||
protected readonly showDialog = signal(false);
|
||||
protected readonly dialog = viewChild<ElementRef<HTMLDialogElement>>('dialog');
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
effect(() => {
|
||||
const dialog = this.dialog();
|
||||
|
||||
if (dialog && !dialog.nativeElement.open) {
|
||||
dialog.nativeElement.showModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected handleDialogClick(event: MouseEvent) {
|
||||
if (event.target instanceof HTMLDialogElement) {
|
||||
this.closeDialog();
|
||||
}
|
||||
}
|
||||
|
||||
protected closeDialog() {
|
||||
const dialog = this.dialog();
|
||||
|
||||
if (!dialog) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dialog.nativeElement.open) {
|
||||
dialog.nativeElement.close();
|
||||
}
|
||||
|
||||
this.showDialog.set(false);
|
||||
}
|
||||
}
|
||||
77
vendor/a2ui/renderers/angular/src/lib/catalog/multiple-choice.ts
vendored
Normal file
77
vendor/a2ui/renderers/angular/src/lib/catalog/multiple-choice.ts
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
Copyright 2025 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 { Component, computed, input } from '@angular/core';
|
||||
import { DynamicComponent } from '../rendering/dynamic-component';
|
||||
import { Primitives } from '@a2ui/lit/0.8';
|
||||
|
||||
@Component({
|
||||
selector: 'a2ui-multiple-choice',
|
||||
template: `
|
||||
<section [class]="theme.components.MultipleChoice.container">
|
||||
<label [class]="theme.components.MultipleChoice.label" [for]="selectId">{{
|
||||
description()
|
||||
}}</label>
|
||||
|
||||
<select
|
||||
(change)="handleChange($event)"
|
||||
[id]="selectId"
|
||||
[value]="selectValue()"
|
||||
[class]="theme.components.MultipleChoice.element"
|
||||
[style]="theme.additionalStyles?.MultipleChoice"
|
||||
>
|
||||
@for (option of options(); track option.value) {
|
||||
<option [value]="option.value">{{ resolvePrimitive(option.label) }}</option>
|
||||
}
|
||||
</select>
|
||||
</section>
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
display: block;
|
||||
flex: var(--weight);
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
select {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
`,
|
||||
})
|
||||
export class MultipleChoice extends DynamicComponent {
|
||||
readonly options = input.required<{ label: Primitives.StringValue; value: string }[]>();
|
||||
readonly value = input.required<Primitives.StringValue | null>();
|
||||
readonly description = input.required<string>();
|
||||
|
||||
protected readonly selectId = super.getUniqueId('a2ui-multiple-choice');
|
||||
protected selectValue = computed(() => super.resolvePrimitive(this.value()));
|
||||
|
||||
protected handleChange(event: Event) {
|
||||
const path = this.value()?.path;
|
||||
|
||||
if (!(event.target instanceof HTMLSelectElement) || !event.target.value || !path) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.processor.setData(
|
||||
this.component(),
|
||||
this.processor.resolvePath(path, this.component().dataContextPath),
|
||||
event.target.value,
|
||||
);
|
||||
}
|
||||
}
|
||||
100
vendor/a2ui/renderers/angular/src/lib/catalog/row.ts
vendored
Normal file
100
vendor/a2ui/renderers/angular/src/lib/catalog/row.ts
vendored
Normal file
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
Copyright 2025 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 { Component, computed, input } from '@angular/core';
|
||||
import { DynamicComponent } from '../rendering/dynamic-component';
|
||||
import { Renderer } from '../rendering/renderer';
|
||||
import { Types } from '@a2ui/lit/0.8';
|
||||
|
||||
@Component({
|
||||
selector: 'a2ui-row',
|
||||
imports: [Renderer],
|
||||
host: {
|
||||
'[attr.alignment]': 'alignment()',
|
||||
'[attr.distribution]': 'distribution()',
|
||||
},
|
||||
styles: `
|
||||
:host {
|
||||
display: flex;
|
||||
flex: var(--weight);
|
||||
}
|
||||
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.align-start {
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.align-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.align-end {
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.align-stretch {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.distribute-start {
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
.distribute-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.distribute-end {
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
.distribute-spaceBetween {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.distribute-spaceAround {
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.distribute-spaceEvenly {
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
`,
|
||||
template: `
|
||||
<section [class]="classes()" [style]="theme.additionalStyles?.Row">
|
||||
@for (child of component().properties.children; track child) {
|
||||
<ng-container a2ui-renderer [surfaceId]="surfaceId()!" [component]="child" />
|
||||
}
|
||||
</section>
|
||||
`,
|
||||
})
|
||||
export class Row extends DynamicComponent<Types.RowNode> {
|
||||
readonly alignment = input<Types.ResolvedRow['alignment']>('stretch');
|
||||
readonly distribution = input<Types.ResolvedRow['distribution']>('start');
|
||||
|
||||
protected readonly classes = computed(() => ({
|
||||
...this.theme.components.Row,
|
||||
[`align-${this.alignment()}`]: true,
|
||||
[`distribute-${this.distribution()}`]: true,
|
||||
}));
|
||||
}
|
||||
73
vendor/a2ui/renderers/angular/src/lib/catalog/slider.ts
vendored
Normal file
73
vendor/a2ui/renderers/angular/src/lib/catalog/slider.ts
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
Copyright 2025 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 { Component, computed, input } from '@angular/core';
|
||||
import { Primitives } from '@a2ui/lit/0.8';
|
||||
import { DynamicComponent } from '../rendering/dynamic-component';
|
||||
|
||||
@Component({
|
||||
selector: '[a2ui-slider]',
|
||||
template: `
|
||||
<section [class]="theme.components.Slider.container">
|
||||
<label [class]="theme.components.Slider.label" [for]="inputId">
|
||||
{{ label() }}
|
||||
</label>
|
||||
|
||||
<input
|
||||
autocomplete="off"
|
||||
type="range"
|
||||
[value]="resolvedValue()"
|
||||
[min]="minValue()"
|
||||
[max]="maxValue()"
|
||||
[id]="inputId"
|
||||
(input)="handleInput($event)"
|
||||
[class]="theme.components.Slider.element"
|
||||
[style]="theme.additionalStyles?.Slider"
|
||||
/>
|
||||
</section>
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
display: block;
|
||||
flex: var(--weight);
|
||||
}
|
||||
|
||||
input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
`,
|
||||
})
|
||||
export class Slider extends DynamicComponent {
|
||||
readonly value = input.required<Primitives.NumberValue | null>();
|
||||
readonly label = input('');
|
||||
readonly minValue = input.required<number | undefined>();
|
||||
readonly maxValue = input.required<number | undefined>();
|
||||
|
||||
protected readonly inputId = super.getUniqueId('a2ui-slider');
|
||||
protected resolvedValue = computed(() => super.resolvePrimitive(this.value()) ?? 0);
|
||||
|
||||
protected handleInput(event: Event) {
|
||||
const path = this.value()?.path;
|
||||
|
||||
if (!(event.target instanceof HTMLInputElement) || !path) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.processor.setData(this.component(), path, event.target.valueAsNumber, this.surfaceId());
|
||||
}
|
||||
}
|
||||
99
vendor/a2ui/renderers/angular/src/lib/catalog/surface.ts
vendored
Normal file
99
vendor/a2ui/renderers/angular/src/lib/catalog/surface.ts
vendored
Normal file
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
Copyright 2025 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 { Component, computed, input } from '@angular/core';
|
||||
import { Types } from '@a2ui/lit/0.8';
|
||||
import { Renderer } from '../rendering/renderer';
|
||||
|
||||
@Component({
|
||||
selector: 'a2ui-surface',
|
||||
imports: [Renderer],
|
||||
template: `
|
||||
@let surfaceId = this.surfaceId();
|
||||
@let surface = this.surface();
|
||||
|
||||
@if (surfaceId && surface) {
|
||||
<ng-container a2ui-renderer [surfaceId]="surfaceId" [component]="surface.componentTree!" />
|
||||
}
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
max-height: 100%;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
`,
|
||||
host: {
|
||||
'[style]': 'styles()',
|
||||
},
|
||||
})
|
||||
export class Surface {
|
||||
readonly surfaceId = input.required<Types.SurfaceID | null>();
|
||||
readonly surface = input.required<Types.Surface | null>();
|
||||
|
||||
protected readonly styles = computed(() => {
|
||||
const surface = this.surface();
|
||||
const styles: Record<string, string> = {};
|
||||
|
||||
if (surface?.styles) {
|
||||
for (const [key, value] of Object.entries(surface.styles)) {
|
||||
switch (key) {
|
||||
// Here we generate a palette from the singular primary color received
|
||||
// from the surface data. We will want the values to range from
|
||||
// 0 <= x <= 100, where 0 = back, 100 = white, and 50 = the primary
|
||||
// color itself. As such we use a color-mix to create the intermediate
|
||||
// values.
|
||||
//
|
||||
// Note: since we use half the range for black to the primary color,
|
||||
// and half the range for primary color to white the mixed values have
|
||||
// to go up double the amount, i.e., a range from black to primary
|
||||
// color needs to fit in 0 -> 50 rather than 0 -> 100.
|
||||
case 'primaryColor': {
|
||||
styles['--p-100'] = '#ffffff';
|
||||
styles['--p-99'] = `color-mix(in srgb, ${value} 2%, white 98%)`;
|
||||
styles['--p-98'] = `color-mix(in srgb, ${value} 4%, white 96%)`;
|
||||
styles['--p-95'] = `color-mix(in srgb, ${value} 10%, white 90%)`;
|
||||
styles['--p-90'] = `color-mix(in srgb, ${value} 20%, white 80%)`;
|
||||
styles['--p-80'] = `color-mix(in srgb, ${value} 40%, white 60%)`;
|
||||
styles['--p-70'] = `color-mix(in srgb, ${value} 60%, white 40%)`;
|
||||
styles['--p-60'] = `color-mix(in srgb, ${value} 80%, white 20%)`;
|
||||
styles['--p-50'] = value;
|
||||
styles['--p-40'] = `color-mix(in srgb, ${value} 80%, black 20%)`;
|
||||
styles['--p-35'] = `color-mix(in srgb, ${value} 70%, black 30%)`;
|
||||
styles['--p-30'] = `color-mix(in srgb, ${value} 60%, black 40%)`;
|
||||
styles['--p-25'] = `color-mix(in srgb, ${value} 50%, black 50%)`;
|
||||
styles['--p-20'] = `color-mix(in srgb, ${value} 40%, black 60%)`;
|
||||
styles['--p-15'] = `color-mix(in srgb, ${value} 30%, black 70%)`;
|
||||
styles['--p-10'] = `color-mix(in srgb, ${value} 20%, black 80%)`;
|
||||
styles['--p-5'] = `color-mix(in srgb, ${value} 10%, black 90%)`;
|
||||
styles['--0'] = '#00000';
|
||||
break;
|
||||
}
|
||||
|
||||
case 'font': {
|
||||
styles['--font-family'] = value;
|
||||
styles['--font-family-flex'] = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return styles;
|
||||
});
|
||||
}
|
||||
72
vendor/a2ui/renderers/angular/src/lib/catalog/tabs.ts
vendored
Normal file
72
vendor/a2ui/renderers/angular/src/lib/catalog/tabs.ts
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
Copyright 2025 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 { Component, computed, input, signal } from '@angular/core';
|
||||
import { DynamicComponent } from '../rendering/dynamic-component';
|
||||
import { Renderer } from '../rendering/renderer';
|
||||
import { Styles, Types } from '@a2ui/lit/0.8';
|
||||
|
||||
@Component({
|
||||
selector: 'a2ui-tabs',
|
||||
imports: [Renderer],
|
||||
template: `
|
||||
@let tabs = this.tabs();
|
||||
@let selectedIndex = this.selectedIndex();
|
||||
|
||||
<section [class]="theme.components.Tabs.container" [style]="theme.additionalStyles?.Tabs">
|
||||
<div [class]="theme.components.Tabs.element">
|
||||
@for (tab of tabs; track tab) {
|
||||
<button
|
||||
(click)="this.selectedIndex.set($index)"
|
||||
[disabled]="selectedIndex === $index"
|
||||
[class]="buttonClasses()[selectedIndex]"
|
||||
>
|
||||
{{ resolvePrimitive(tab.title) }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<ng-container
|
||||
a2ui-renderer
|
||||
[surfaceId]="surfaceId()!"
|
||||
[component]="tabs[selectedIndex].child"
|
||||
/>
|
||||
</section>
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
display: block;
|
||||
flex: var(--weight);
|
||||
}
|
||||
`,
|
||||
})
|
||||
export class Tabs extends DynamicComponent {
|
||||
protected selectedIndex = signal(0);
|
||||
readonly tabs = input.required<Types.ResolvedTabItem[]>();
|
||||
|
||||
protected readonly buttonClasses = computed(() => {
|
||||
const selectedIndex = this.selectedIndex();
|
||||
|
||||
return this.tabs().map((_, index) => {
|
||||
return index === selectedIndex
|
||||
? Styles.merge(
|
||||
this.theme.components.Tabs.controls.all,
|
||||
this.theme.components.Tabs.controls.selected,
|
||||
)
|
||||
: this.theme.components.Tabs.controls.all;
|
||||
});
|
||||
});
|
||||
}
|
||||
86
vendor/a2ui/renderers/angular/src/lib/catalog/text-field.ts
vendored
Normal file
86
vendor/a2ui/renderers/angular/src/lib/catalog/text-field.ts
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
Copyright 2025 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 { computed, Component, input } from '@angular/core';
|
||||
import { Primitives, Types } from '@a2ui/lit/0.8';
|
||||
import { DynamicComponent } from '../rendering/dynamic-component';
|
||||
|
||||
@Component({
|
||||
selector: 'a2ui-text-field',
|
||||
styles: `
|
||||
:host {
|
||||
display: flex;
|
||||
flex: var(--weight);
|
||||
}
|
||||
|
||||
section,
|
||||
input,
|
||||
label {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
`,
|
||||
template: `
|
||||
@let resolvedLabel = this.resolvedLabel();
|
||||
|
||||
<section [class]="theme.components.TextField.container">
|
||||
@if (resolvedLabel) {
|
||||
<label [for]="inputId" [class]="theme.components.TextField.label">{{
|
||||
resolvedLabel
|
||||
}}</label>
|
||||
}
|
||||
|
||||
<input
|
||||
autocomplete="off"
|
||||
[class]="theme.components.TextField.element"
|
||||
[style]="theme.additionalStyles?.TextField"
|
||||
(input)="handleInput($event)"
|
||||
[id]="inputId"
|
||||
[value]="inputValue()"
|
||||
placeholder="Please enter a value"
|
||||
[type]="inputType() === 'number' ? 'number' : 'text'"
|
||||
/>
|
||||
</section>
|
||||
`,
|
||||
})
|
||||
export class TextField extends DynamicComponent {
|
||||
readonly text = input.required<Primitives.StringValue | null>();
|
||||
readonly label = input.required<Primitives.StringValue | null>();
|
||||
readonly inputType = input.required<Types.ResolvedTextField['type'] | null>();
|
||||
|
||||
protected inputValue = computed(() => super.resolvePrimitive(this.text()) || '');
|
||||
protected resolvedLabel = computed(() => super.resolvePrimitive(this.label()));
|
||||
protected inputId = super.getUniqueId('a2ui-input');
|
||||
|
||||
protected handleInput(event: Event) {
|
||||
const path = this.text()?.path;
|
||||
|
||||
if (!(event.target instanceof HTMLInputElement) || !path) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.processor.setData(this.component(), path, event.target.value, this.surfaceId());
|
||||
}
|
||||
}
|
||||
137
vendor/a2ui/renderers/angular/src/lib/catalog/text.ts
vendored
Normal file
137
vendor/a2ui/renderers/angular/src/lib/catalog/text.ts
vendored
Normal file
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
Copyright 2025 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 { Component, computed, inject, input, ViewEncapsulation } from '@angular/core';
|
||||
import { DynamicComponent } from '../rendering/dynamic-component';
|
||||
import { Primitives, Styles, Types } from '@a2ui/lit/0.8';
|
||||
import { MarkdownRenderer } from '../data/markdown';
|
||||
|
||||
interface HintedStyles {
|
||||
h1: Record<string, string>;
|
||||
h2: Record<string, string>;
|
||||
h3: Record<string, string>;
|
||||
h4: Record<string, string>;
|
||||
h5: Record<string, string>;
|
||||
body: Record<string, string>;
|
||||
caption: Record<string, string>;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'a2ui-text',
|
||||
template: `
|
||||
<section
|
||||
[class]="classes()"
|
||||
[style]="additionalStyles()"
|
||||
[innerHTML]="resolvedText()"
|
||||
></section>
|
||||
`,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
styles: `
|
||||
a2ui-text {
|
||||
display: block;
|
||||
flex: var(--weight);
|
||||
}
|
||||
|
||||
a2ui-text h1,
|
||||
a2ui-text h2,
|
||||
a2ui-text h3,
|
||||
a2ui-text h4,
|
||||
a2ui-text h5 {
|
||||
line-height: inherit;
|
||||
font: inherit;
|
||||
}
|
||||
`,
|
||||
})
|
||||
export class Text extends DynamicComponent {
|
||||
private markdownRenderer = inject(MarkdownRenderer);
|
||||
readonly text = input.required<Primitives.StringValue | null>();
|
||||
readonly usageHint = input.required<Types.ResolvedText['usageHint'] | null>();
|
||||
|
||||
protected resolvedText = computed(() => {
|
||||
const usageHint = this.usageHint();
|
||||
let value = super.resolvePrimitive(this.text());
|
||||
|
||||
if (value == null) {
|
||||
return '(empty)';
|
||||
}
|
||||
|
||||
switch (usageHint) {
|
||||
case 'h1':
|
||||
value = `# ${value}`;
|
||||
break;
|
||||
case 'h2':
|
||||
value = `## ${value}`;
|
||||
break;
|
||||
case 'h3':
|
||||
value = `### ${value}`;
|
||||
break;
|
||||
case 'h4':
|
||||
value = `#### ${value}`;
|
||||
break;
|
||||
case 'h5':
|
||||
value = `##### ${value}`;
|
||||
break;
|
||||
case 'caption':
|
||||
value = `*${value}*`;
|
||||
break;
|
||||
default:
|
||||
value = String(value);
|
||||
break;
|
||||
}
|
||||
|
||||
return this.markdownRenderer.render(
|
||||
value,
|
||||
Styles.appendToAll(this.theme.markdown, ['ol', 'ul', 'li'], {}),
|
||||
);
|
||||
});
|
||||
|
||||
protected classes = computed(() => {
|
||||
const usageHint = this.usageHint();
|
||||
|
||||
return Styles.merge(
|
||||
this.theme.components.Text.all,
|
||||
usageHint ? this.theme.components.Text[usageHint] : {},
|
||||
);
|
||||
});
|
||||
|
||||
protected additionalStyles = computed(() => {
|
||||
const usageHint = this.usageHint();
|
||||
const styles = this.theme.additionalStyles?.Text;
|
||||
|
||||
if (!styles) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let additionalStyles: Record<string, string> = {};
|
||||
|
||||
if (this.areHintedStyles(styles)) {
|
||||
additionalStyles = styles[usageHint ?? 'body'];
|
||||
} else {
|
||||
additionalStyles = styles;
|
||||
}
|
||||
|
||||
return additionalStyles;
|
||||
});
|
||||
|
||||
private areHintedStyles(styles: unknown): styles is HintedStyles {
|
||||
if (typeof styles !== 'object' || !styles || Array.isArray(styles)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const expected = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'caption', 'body'];
|
||||
return expected.every((v) => v in styles);
|
||||
}
|
||||
}
|
||||
50
vendor/a2ui/renderers/angular/src/lib/catalog/video.ts
vendored
Normal file
50
vendor/a2ui/renderers/angular/src/lib/catalog/video.ts
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
Copyright 2025 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 { Component, computed, input } from '@angular/core';
|
||||
import { DynamicComponent } from '../rendering/dynamic-component';
|
||||
import { Primitives } from '@a2ui/lit/0.8';
|
||||
|
||||
@Component({
|
||||
selector: 'a2ui-video',
|
||||
template: `
|
||||
@let resolvedUrl = this.resolvedUrl();
|
||||
|
||||
@if (resolvedUrl) {
|
||||
<section [class]="theme.components.Video" [style]="theme.additionalStyles?.Video">
|
||||
<video controls [src]="resolvedUrl"></video>
|
||||
</section>
|
||||
}
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
display: block;
|
||||
flex: var(--weight);
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
video {
|
||||
display: block;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
`,
|
||||
})
|
||||
export class Video extends DynamicComponent {
|
||||
readonly url = input.required<Primitives.StringValue | null>();
|
||||
protected readonly resolvedUrl = computed(() => this.resolvePrimitive(this.url()));
|
||||
}
|
||||
25
vendor/a2ui/renderers/angular/src/lib/config.ts
vendored
Normal file
25
vendor/a2ui/renderers/angular/src/lib/config.ts
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
Copyright 2025 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 { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core';
|
||||
import { Catalog, Theme } from './rendering';
|
||||
|
||||
export function provideA2UI(config: { catalog: Catalog; theme: Theme }): EnvironmentProviders {
|
||||
return makeEnvironmentProviders([
|
||||
{ provide: Catalog, useValue: config.catalog },
|
||||
{ provide: Theme, useValue: config.theme },
|
||||
]);
|
||||
}
|
||||
18
vendor/a2ui/renderers/angular/src/lib/data/index.ts
vendored
Normal file
18
vendor/a2ui/renderers/angular/src/lib/data/index.ts
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
Copyright 2025 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.
|
||||
*/
|
||||
|
||||
export * from './processor';
|
||||
export * from './types';
|
||||
114
vendor/a2ui/renderers/angular/src/lib/data/markdown.ts
vendored
Normal file
114
vendor/a2ui/renderers/angular/src/lib/data/markdown.ts
vendored
Normal file
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
Copyright 2025 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 { inject, Injectable, SecurityContext } from '@angular/core';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
import MarkdownIt from 'markdown-it';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class MarkdownRenderer {
|
||||
private originalClassMap = new Map<string, any>();
|
||||
private sanitizer = inject(DomSanitizer);
|
||||
|
||||
private markdownIt = MarkdownIt({
|
||||
highlight: (str, lang) => {
|
||||
if (lang === 'html') {
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.classList.add('html-view');
|
||||
iframe.srcdoc = str;
|
||||
iframe.sandbox = '';
|
||||
return iframe.innerHTML;
|
||||
}
|
||||
|
||||
return str;
|
||||
},
|
||||
});
|
||||
|
||||
render(value: string, tagClassMap?: Record<string, string[]>) {
|
||||
if (tagClassMap) {
|
||||
this.applyTagClassMap(tagClassMap);
|
||||
}
|
||||
const htmlString = this.markdownIt.render(value);
|
||||
this.unapplyTagClassMap();
|
||||
return this.sanitizer.sanitize(SecurityContext.HTML, htmlString);
|
||||
}
|
||||
|
||||
private applyTagClassMap(tagClassMap: Record<string, string[]>) {
|
||||
Object.entries(tagClassMap).forEach(([tag, classes]) => {
|
||||
let tokenName;
|
||||
switch (tag) {
|
||||
case 'p':
|
||||
tokenName = 'paragraph';
|
||||
break;
|
||||
case 'h1':
|
||||
case 'h2':
|
||||
case 'h3':
|
||||
case 'h4':
|
||||
case 'h5':
|
||||
case 'h6':
|
||||
tokenName = 'heading';
|
||||
break;
|
||||
case 'ul':
|
||||
tokenName = 'bullet_list';
|
||||
break;
|
||||
case 'ol':
|
||||
tokenName = 'ordered_list';
|
||||
break;
|
||||
case 'li':
|
||||
tokenName = 'list_item';
|
||||
break;
|
||||
case 'a':
|
||||
tokenName = 'link';
|
||||
break;
|
||||
case 'strong':
|
||||
tokenName = 'strong';
|
||||
break;
|
||||
case 'em':
|
||||
tokenName = 'em';
|
||||
break;
|
||||
}
|
||||
|
||||
if (!tokenName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = `${tokenName}_open`;
|
||||
const original = this.markdownIt.renderer.rules[key];
|
||||
this.originalClassMap.set(key, original);
|
||||
|
||||
this.markdownIt.renderer.rules[key] = (tokens, idx, options, env, self) => {
|
||||
const token = tokens[idx];
|
||||
for (const clazz of classes) {
|
||||
token.attrJoin('class', clazz);
|
||||
}
|
||||
|
||||
if (original) {
|
||||
return original.call(this, tokens, idx, options, env, self);
|
||||
} else {
|
||||
return self.renderToken(tokens, idx, options);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private unapplyTagClassMap() {
|
||||
for (const [key, original] of this.originalClassMap) {
|
||||
this.markdownIt.renderer.rules[key] = original;
|
||||
}
|
||||
|
||||
this.originalClassMap.clear();
|
||||
}
|
||||
}
|
||||
47
vendor/a2ui/renderers/angular/src/lib/data/processor.ts
vendored
Normal file
47
vendor/a2ui/renderers/angular/src/lib/data/processor.ts
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
Copyright 2025 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 { Data, Types } from '@a2ui/lit/0.8';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { firstValueFrom, Subject } from 'rxjs';
|
||||
|
||||
export interface DispatchedEvent {
|
||||
message: Types.A2UIClientEventMessage;
|
||||
completion: Subject<Types.ServerToClientMessage[]>;
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class MessageProcessor extends Data.A2uiMessageProcessor {
|
||||
readonly events = new Subject<DispatchedEvent>();
|
||||
|
||||
override setData(
|
||||
node: Types.AnyComponentNode,
|
||||
relativePath: string,
|
||||
value: Types.DataValue,
|
||||
surfaceId?: Types.SurfaceID | null,
|
||||
) {
|
||||
// Override setData to convert from optional inputs (which can be null)
|
||||
// to undefined so that this correctly falls back to the default value for
|
||||
// surfaceId.
|
||||
return super.setData(node, relativePath, value, surfaceId ?? undefined);
|
||||
}
|
||||
|
||||
dispatch(message: Types.A2UIClientEventMessage): Promise<Types.ServerToClientMessage[]> {
|
||||
const completion = new Subject<Types.ServerToClientMessage[]>();
|
||||
this.events.next({ message, completion });
|
||||
return firstValueFrom(completion);
|
||||
}
|
||||
}
|
||||
29
vendor/a2ui/renderers/angular/src/lib/data/types.ts
vendored
Normal file
29
vendor/a2ui/renderers/angular/src/lib/data/types.ts
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
Copyright 2025 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 { Types } from '@a2ui/lit/0.8';
|
||||
|
||||
export interface A2TextPayload {
|
||||
kind: 'text';
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface A2DataPayload {
|
||||
kind: 'data';
|
||||
data: Types.ServerToClientMessage;
|
||||
}
|
||||
|
||||
export type A2AServerPayload = Array<A2DataPayload | A2TextPayload> | { error: string };
|
||||
36
vendor/a2ui/renderers/angular/src/lib/rendering/catalog.ts
vendored
Normal file
36
vendor/a2ui/renderers/angular/src/lib/rendering/catalog.ts
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
Copyright 2025 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 { Binding, InjectionToken, Type } from '@angular/core';
|
||||
import { DynamicComponent } from './dynamic-component';
|
||||
import { Types } from '@a2ui/lit/0.8';
|
||||
|
||||
export type CatalogLoader = () =>
|
||||
| Promise<Type<DynamicComponent<any>>>
|
||||
| Type<DynamicComponent<any>>;
|
||||
|
||||
export type CatalogEntry<T extends Types.AnyComponentNode> =
|
||||
| CatalogLoader
|
||||
| {
|
||||
type: CatalogLoader;
|
||||
bindings: (data: T) => Binding[];
|
||||
};
|
||||
|
||||
export interface Catalog {
|
||||
[key: string]: CatalogEntry<Types.AnyComponentNode>;
|
||||
}
|
||||
|
||||
export const Catalog = new InjectionToken<Catalog>('Catalog');
|
||||
100
vendor/a2ui/renderers/angular/src/lib/rendering/dynamic-component.ts
vendored
Normal file
100
vendor/a2ui/renderers/angular/src/lib/rendering/dynamic-component.ts
vendored
Normal file
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
Copyright 2025 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 { Types, Primitives } from '@a2ui/lit/0.8';
|
||||
import { Directive, inject, input } from '@angular/core';
|
||||
import { MessageProcessor } from '../data';
|
||||
import { Theme } from './theming';
|
||||
|
||||
let idCounter = 0;
|
||||
|
||||
@Directive({
|
||||
host: {
|
||||
'[style.--weight]': 'weight()',
|
||||
},
|
||||
})
|
||||
export abstract class DynamicComponent<T extends Types.AnyComponentNode = Types.AnyComponentNode> {
|
||||
protected readonly processor = inject(MessageProcessor);
|
||||
protected readonly theme = inject(Theme);
|
||||
|
||||
readonly surfaceId = input.required<Types.SurfaceID | null>();
|
||||
readonly component = input.required<T>();
|
||||
readonly weight = input.required<string | number>();
|
||||
|
||||
protected sendAction(action: Types.Action): Promise<Types.ServerToClientMessage[]> {
|
||||
const component = this.component();
|
||||
const surfaceId = this.surfaceId() ?? undefined;
|
||||
const context: Record<string, unknown> = {};
|
||||
|
||||
if (action.context) {
|
||||
for (const item of action.context) {
|
||||
if (item.value.literalBoolean) {
|
||||
context[item.key] = item.value.literalBoolean;
|
||||
} else if (item.value.literalNumber) {
|
||||
context[item.key] = item.value.literalNumber;
|
||||
} else if (item.value.literalString) {
|
||||
context[item.key] = item.value.literalString;
|
||||
} else if (item.value.path) {
|
||||
const path = this.processor.resolvePath(item.value.path, component.dataContextPath);
|
||||
const value = this.processor.getData(component, path, surfaceId);
|
||||
context[item.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const message: Types.A2UIClientEventMessage = {
|
||||
userAction: {
|
||||
name: action.name,
|
||||
sourceComponentId: component.id,
|
||||
surfaceId: surfaceId!,
|
||||
timestamp: new Date().toISOString(),
|
||||
context,
|
||||
},
|
||||
};
|
||||
|
||||
return this.processor.dispatch(message);
|
||||
}
|
||||
|
||||
protected resolvePrimitive(value: Primitives.StringValue | null): string | null;
|
||||
protected resolvePrimitive(value: Primitives.BooleanValue | null): boolean | null;
|
||||
protected resolvePrimitive(value: Primitives.NumberValue | null): number | null;
|
||||
protected resolvePrimitive(
|
||||
value: Primitives.StringValue | Primitives.BooleanValue | Primitives.NumberValue | null,
|
||||
) {
|
||||
const component = this.component();
|
||||
const surfaceId = this.surfaceId();
|
||||
|
||||
if (!value || typeof value !== 'object') {
|
||||
return null;
|
||||
} else if (value.literal != null) {
|
||||
return value.literal;
|
||||
} else if (value.path) {
|
||||
return this.processor.getData(component, value.path, surfaceId ?? undefined);
|
||||
} else if ('literalString' in value) {
|
||||
return value.literalString;
|
||||
} else if ('literalNumber' in value) {
|
||||
return value.literalNumber;
|
||||
} else if ('literalBoolean' in value) {
|
||||
return value.literalBoolean;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected getUniqueId(prefix: string) {
|
||||
return `${prefix}-${idCounter++}`;
|
||||
}
|
||||
}
|
||||
20
vendor/a2ui/renderers/angular/src/lib/rendering/index.ts
vendored
Normal file
20
vendor/a2ui/renderers/angular/src/lib/rendering/index.ts
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
Copyright 2025 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.
|
||||
*/
|
||||
|
||||
export * from './catalog';
|
||||
export * from './dynamic-component';
|
||||
export * from './renderer';
|
||||
export * from './theming';
|
||||
109
vendor/a2ui/renderers/angular/src/lib/rendering/renderer.ts
vendored
Normal file
109
vendor/a2ui/renderers/angular/src/lib/rendering/renderer.ts
vendored
Normal file
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
Copyright 2025 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 {
|
||||
Binding,
|
||||
ComponentRef,
|
||||
Directive,
|
||||
DOCUMENT,
|
||||
effect,
|
||||
inject,
|
||||
input,
|
||||
inputBinding,
|
||||
OnDestroy,
|
||||
PLATFORM_ID,
|
||||
Type,
|
||||
untracked,
|
||||
ViewContainerRef,
|
||||
} from '@angular/core';
|
||||
import { Types, Styles } from '@a2ui/lit/0.8';
|
||||
import { Catalog } from './catalog';
|
||||
import { isPlatformBrowser } from '@angular/common';
|
||||
|
||||
@Directive({
|
||||
selector: 'ng-container[a2ui-renderer]',
|
||||
})
|
||||
export class Renderer implements OnDestroy {
|
||||
private viewContainerRef = inject(ViewContainerRef);
|
||||
private catalog = inject(Catalog);
|
||||
private static hasInsertedStyles = false;
|
||||
|
||||
private currentRef: ComponentRef<unknown> | null = null;
|
||||
private isDestroyed = false;
|
||||
|
||||
readonly surfaceId = input.required<Types.SurfaceID>();
|
||||
readonly component = input.required<Types.AnyComponentNode>();
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
const surfaceId = this.surfaceId();
|
||||
const component = this.component();
|
||||
untracked(() => this.render(surfaceId, component));
|
||||
});
|
||||
|
||||
const platformId = inject(PLATFORM_ID);
|
||||
const document = inject(DOCUMENT);
|
||||
|
||||
if (!Renderer.hasInsertedStyles && isPlatformBrowser(platformId)) {
|
||||
const styles = document.createElement('style');
|
||||
styles.textContent = Styles.structuralStyles;
|
||||
document.head.appendChild(styles);
|
||||
Renderer.hasInsertedStyles = true;
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.isDestroyed = true;
|
||||
this.clear();
|
||||
}
|
||||
|
||||
private async render(surfaceId: Types.SurfaceID, component: Types.AnyComponentNode) {
|
||||
const config = this.catalog[component.type];
|
||||
let newComponent: Type<unknown> | null = null;
|
||||
let componentBindings: Binding[] | null = null;
|
||||
|
||||
if (typeof config === 'function') {
|
||||
newComponent = await config();
|
||||
} else if (typeof config === 'object') {
|
||||
newComponent = await config.type();
|
||||
componentBindings = config.bindings(component as any);
|
||||
}
|
||||
|
||||
this.clear();
|
||||
|
||||
if (newComponent && !this.isDestroyed) {
|
||||
const bindings = [
|
||||
inputBinding('surfaceId', () => surfaceId),
|
||||
inputBinding('component', () => component),
|
||||
inputBinding('weight', () => component.weight ?? 'initial'),
|
||||
];
|
||||
|
||||
if (componentBindings) {
|
||||
bindings.push(...componentBindings);
|
||||
}
|
||||
|
||||
this.currentRef = this.viewContainerRef.createComponent(newComponent, {
|
||||
bindings,
|
||||
injector: this.viewContainerRef.injector,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private clear() {
|
||||
this.currentRef?.destroy();
|
||||
this.currentRef = null;
|
||||
}
|
||||
}
|
||||
22
vendor/a2ui/renderers/angular/src/lib/rendering/theming.ts
vendored
Normal file
22
vendor/a2ui/renderers/angular/src/lib/rendering/theming.ts
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
Copyright 2025 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 { Types } from '@a2ui/lit/0.8';
|
||||
import { InjectionToken } from '@angular/core';
|
||||
|
||||
export const Theme = new InjectionToken<Theme>('Theme');
|
||||
|
||||
export type Theme = Types.Theme;
|
||||
21
vendor/a2ui/renderers/angular/src/public-api.ts
vendored
Normal file
21
vendor/a2ui/renderers/angular/src/public-api.ts
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
Copyright 2025 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.
|
||||
*/
|
||||
|
||||
export * from './lib/rendering/index';
|
||||
export * from './lib/data/index';
|
||||
export * from './lib/config';
|
||||
export * from './lib/catalog/default';
|
||||
export { Surface } from './lib/catalog/surface';
|
||||
Reference in New Issue
Block a user