From a183f16f6e412f70d4ca29bf59b34c17d521912c Mon Sep 17 00:00:00 2001 From: Jose Montes Date: Fri, 7 Aug 2026 22:32:27 -0500 Subject: [PATCH] refactor(angular): remove legacy basic catalog component implementations --- renderers/angular/CHANGELOG.md | 2 + .../basic/audio-player.component.spec.ts | 93 --------- .../catalog/basic/audio-player.component.ts | 72 ------- .../basic/basic-catalog-component.spec.ts | 92 --------- .../catalog/basic/basic-catalog-component.ts | 78 -------- .../catalog/basic/button.component.spec.ts | 170 ---------------- .../v0_9/catalog/basic/button.component.ts | 114 ----------- .../v0_9/catalog/basic/card.component.spec.ts | 92 --------- .../src/v0_9/catalog/basic/card.component.ts | 66 ------- .../catalog/basic/check-box.component.spec.ts | 112 ----------- .../v0_9/catalog/basic/check-box.component.ts | 92 --------- .../basic/choice-picker.component.spec.ts | 136 ------------- .../catalog/basic/choice-picker.component.ts | 173 ---------------- .../catalog/basic/column.component.spec.ts | 187 ------------------ .../v0_9/catalog/basic/column.component.ts | 68 ------- .../basic/date-time-input.component.spec.ts | 122 ------------ .../basic/date-time-input.component.ts | 143 -------------- .../catalog/basic/divider.component.spec.ts | 83 -------- .../v0_9/catalog/basic/divider.component.ts | 72 ------- .../v0_9/catalog/basic/icon.component.spec.ts | 135 ------------- .../src/v0_9/catalog/basic/icon.component.ts | 109 ---------- .../catalog/basic/image.component.spec.ts | 107 ---------- .../src/v0_9/catalog/basic/image.component.ts | 81 -------- .../v0_9/catalog/basic/list.component.spec.ts | 140 ------------- .../src/v0_9/catalog/basic/list.component.ts | 120 ----------- .../catalog/basic/modal.component.spec.ts | 157 --------------- .../src/v0_9/catalog/basic/modal.component.ts | 133 ------------- .../v0_9/catalog/basic/row.component.spec.ts | 156 --------------- .../src/v0_9/catalog/basic/row.component.ts | 67 ------- .../catalog/basic/slider.component.spec.ts | 96 --------- .../v0_9/catalog/basic/slider.component.ts | 92 --------- .../v0_9/catalog/basic/tabs.component.spec.ts | 122 ------------ .../src/v0_9/catalog/basic/tabs.component.ts | 116 ----------- .../basic/text-field.component.spec.ts | 136 ------------- .../catalog/basic/text-field.component.ts | 118 ----------- .../v0_9/catalog/basic/text.component.spec.ts | 181 ----------------- .../src/v0_9/catalog/basic/text.component.ts | 172 ---------------- .../angular/src/v0_9/catalog/basic/utils.ts | 39 ---- .../catalog/basic/video.component.spec.ts | 80 -------- .../src/v0_9/catalog/basic/video.component.ts | 58 ------ 40 files changed, 2 insertions(+), 4380 deletions(-) delete mode 100644 renderers/angular/src/v0_9/catalog/basic/audio-player.component.spec.ts delete mode 100644 renderers/angular/src/v0_9/catalog/basic/audio-player.component.ts delete mode 100644 renderers/angular/src/v0_9/catalog/basic/basic-catalog-component.spec.ts delete mode 100644 renderers/angular/src/v0_9/catalog/basic/basic-catalog-component.ts delete mode 100644 renderers/angular/src/v0_9/catalog/basic/button.component.spec.ts delete mode 100644 renderers/angular/src/v0_9/catalog/basic/button.component.ts delete mode 100644 renderers/angular/src/v0_9/catalog/basic/card.component.spec.ts delete mode 100644 renderers/angular/src/v0_9/catalog/basic/card.component.ts delete mode 100644 renderers/angular/src/v0_9/catalog/basic/check-box.component.spec.ts delete mode 100644 renderers/angular/src/v0_9/catalog/basic/check-box.component.ts delete mode 100644 renderers/angular/src/v0_9/catalog/basic/choice-picker.component.spec.ts delete mode 100644 renderers/angular/src/v0_9/catalog/basic/choice-picker.component.ts delete mode 100644 renderers/angular/src/v0_9/catalog/basic/column.component.spec.ts delete mode 100644 renderers/angular/src/v0_9/catalog/basic/column.component.ts delete mode 100644 renderers/angular/src/v0_9/catalog/basic/date-time-input.component.spec.ts delete mode 100644 renderers/angular/src/v0_9/catalog/basic/date-time-input.component.ts delete mode 100644 renderers/angular/src/v0_9/catalog/basic/divider.component.spec.ts delete mode 100644 renderers/angular/src/v0_9/catalog/basic/divider.component.ts delete mode 100644 renderers/angular/src/v0_9/catalog/basic/icon.component.spec.ts delete mode 100644 renderers/angular/src/v0_9/catalog/basic/icon.component.ts delete mode 100644 renderers/angular/src/v0_9/catalog/basic/image.component.spec.ts delete mode 100644 renderers/angular/src/v0_9/catalog/basic/image.component.ts delete mode 100644 renderers/angular/src/v0_9/catalog/basic/list.component.spec.ts delete mode 100644 renderers/angular/src/v0_9/catalog/basic/list.component.ts delete mode 100644 renderers/angular/src/v0_9/catalog/basic/modal.component.spec.ts delete mode 100644 renderers/angular/src/v0_9/catalog/basic/modal.component.ts delete mode 100644 renderers/angular/src/v0_9/catalog/basic/row.component.spec.ts delete mode 100644 renderers/angular/src/v0_9/catalog/basic/row.component.ts delete mode 100644 renderers/angular/src/v0_9/catalog/basic/slider.component.spec.ts delete mode 100644 renderers/angular/src/v0_9/catalog/basic/slider.component.ts delete mode 100644 renderers/angular/src/v0_9/catalog/basic/tabs.component.spec.ts delete mode 100644 renderers/angular/src/v0_9/catalog/basic/tabs.component.ts delete mode 100644 renderers/angular/src/v0_9/catalog/basic/text-field.component.spec.ts delete mode 100644 renderers/angular/src/v0_9/catalog/basic/text-field.component.ts delete mode 100644 renderers/angular/src/v0_9/catalog/basic/text.component.spec.ts delete mode 100644 renderers/angular/src/v0_9/catalog/basic/text.component.ts delete mode 100644 renderers/angular/src/v0_9/catalog/basic/utils.ts delete mode 100644 renderers/angular/src/v0_9/catalog/basic/video.component.spec.ts delete mode 100644 renderers/angular/src/v0_9/catalog/basic/video.component.ts diff --git a/renderers/angular/CHANGELOG.md b/renderers/angular/CHANGELOG.md index 30acb756e7..b4cb66776e 100644 --- a/renderers/angular/CHANGELOG.md +++ b/renderers/angular/CHANGELOG.md @@ -1,9 +1,11 @@ ## Unreleased +- (v0_9) Remove legacy Angular component implementations of the Basic Catalog in favor of universal Web Components from `@a2ui/web_core`. - (v0_9) Support universal Web Components in the v0.9 renderer: - Migrate default Basic Catalog implementations from Angular components to universal Web Components from `@a2ui/web_core`. - Add `toWebComponent` adapter to convert custom Angular `@Component` implementations into Web Components. - Update `SurfaceComponent` and `AngularCatalog` to dynamically render custom element tags and manage component lifecycle reactively. +- (v0_9) Implement `createComponentImplementation` helper and deprecate `extraComponents` and `functions` in `BasicCatalogOptions` to align with the core API. [#2060](https://github.com/a2ui-project/a2ui/pull/2060) ## 0.10.5 diff --git a/renderers/angular/src/v0_9/catalog/basic/audio-player.component.spec.ts b/renderers/angular/src/v0_9/catalog/basic/audio-player.component.spec.ts deleted file mode 100644 index db17a44825..0000000000 --- a/renderers/angular/src/v0_9/catalog/basic/audio-player.component.spec.ts +++ /dev/null @@ -1,93 +0,0 @@ -/** - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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 {ComponentFixture, TestBed} from '@angular/core/testing'; -import {AudioPlayerComponent} from './audio-player.component'; -import {A2uiRendererService} from '../../core/a2ui-renderer.service'; -import {ComponentBinder} from '../../core/component-binder.service'; -import {setComponentProps, createBoundProperty, ComponentToProps} from '@a2ui/angular/testing'; - -describe('AudioPlayerComponent', () => { - let component: AudioPlayerComponent; - let fixture: ComponentFixture; - let defaultProps: ComponentToProps; - - beforeEach(async () => { - const mockRendererService = { - surfaceGroup: { - getSurface: jasmine.createSpy('getSurface').and.returnValue({ - componentsModel: new Map(), - catalog: { - id: 'mock-catalog', - components: new Map(), - }, - }), - }, - }; - const mockBinder = jasmine.createSpyObj('ComponentBinder', ['bind']); - - await TestBed.configureTestingModule({ - imports: [AudioPlayerComponent], - providers: [ - {provide: A2uiRendererService, useValue: mockRendererService}, - {provide: ComponentBinder, useValue: mockBinder}, - ], - }).compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(AudioPlayerComponent); - component = fixture.componentInstance; - fixture.componentRef.setInput('surfaceId', 'test-surface'); - fixture.componentRef.setInput('dataContextPath', '/'); - - defaultProps = { - url: createBoundProperty('https://example.com/audio.mp3'), - description: createBoundProperty('Test Audio'), - }; - setComponentProps(fixture, defaultProps); - }); - - it('should create', () => { - fixture.detectChanges(); - expect(component).toBeTruthy(); - }); - - it('should render audio with url', () => { - fixture.detectChanges(); - const audio = fixture.nativeElement.querySelector('audio') as HTMLAudioElement; - expect(audio.src).toBeTruthy(); - const desc = fixture.nativeElement.querySelector('.a2ui-audio-description'); - expect(desc?.textContent?.trim()).toBe('Test Audio'); - }); - - it('should not render description if not provided', () => { - setComponentProps(fixture, { - ...defaultProps, - description: createBoundProperty(undefined), - }); - fixture.detectChanges(); - const desc = fixture.nativeElement.querySelector('.a2ui-audio-description'); - expect(desc).toBeFalsy(); - }); - - it('should handle missing props', () => { - setComponentProps(fixture, {} as ComponentToProps); - fixture.detectChanges(); - const audio = fixture.nativeElement.querySelector('audio') as HTMLAudioElement; - expect(audio.getAttribute('src')).toBeFalsy(); - }); -}); diff --git a/renderers/angular/src/v0_9/catalog/basic/audio-player.component.ts b/renderers/angular/src/v0_9/catalog/basic/audio-player.component.ts deleted file mode 100644 index f284de4b21..0000000000 --- a/renderers/angular/src/v0_9/catalog/basic/audio-player.component.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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, ChangeDetectionStrategy} from '@angular/core'; -import {BasicCatalogComponent} from './basic-catalog-component'; -import {AudioPlayerApi} from '@a2ui/web_core/v0_9/basic_catalog'; - -/** - * Angular implementation of the A2UI AudioPlayer component (v0.9). - * - * Renders an audio player with standard controls and an optional description. - * - * Supported CSS variables: - * - `--a2ui-audioplayer-background`: Controls the background of the player. Defaults to `transparent`. - * - `--a2ui-audioplayer-border-radius`: Controls the border radius. Defaults to `0`. - * - `--a2ui-audioplayer-padding`: Controls the padding. Defaults to `0`. - */ -@Component({ - selector: 'a2ui-v09-audio-player', - standalone: true, - imports: [], - template: ` -
- @if (description()) { -
- {{ description() }} -
- } - -
- `, - styles: [ - ` - .a2ui-audio-player { - display: flex; - flex-direction: column; - gap: var(--a2ui-spacing-xs, 0.25rem); - background: var(--a2ui-audioplayer-background, transparent); - border-radius: var(--a2ui-audioplayer-border-radius, 0); - padding: var(--a2ui-audioplayer-padding, 0); - width: 100%; - } - .a2ui-audio-description { - font-size: var(--a2ui-font-size-s, 0.875rem); - color: var(--a2ui-text-caption-color, light-dark(#666, #aaa)); - } - .a2ui-audio { - width: 100%; - } - `, - ], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class AudioPlayerComponent extends BasicCatalogComponent { - readonly description = computed(() => this.props()['description']?.value()); - readonly url = computed(() => this.props()['url']?.value()); -} diff --git a/renderers/angular/src/v0_9/catalog/basic/basic-catalog-component.spec.ts b/renderers/angular/src/v0_9/catalog/basic/basic-catalog-component.spec.ts deleted file mode 100644 index 0562369a39..0000000000 --- a/renderers/angular/src/v0_9/catalog/basic/basic-catalog-component.spec.ts +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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 {ComponentFixture, TestBed} from '@angular/core/testing'; -import {BasicCatalogComponent} from './basic-catalog-component'; -import {A2uiRendererService, A2UI_RENDERER_CONFIG} from '../../core/a2ui-renderer.service'; -import {BasicCatalog} from './basic-catalog'; -import {ComponentApi} from '@a2ui/web_core/v0_9'; -import z from 'zod'; - -export const TestComponentApi = { - name: 'TestComponent', - schema: z - .object({ - foo: z.string(), - }) - .strict(), -} satisfies ComponentApi; - -@Component({ - selector: 'test-basic-comp', - template: '
Test
', - standalone: true, -}) -class TestBasicComp extends BasicCatalogComponent {} - -describe('BasicCatalogComponent', () => { - let fixture: ComponentFixture; - let rendererService: A2uiRendererService; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [TestBasicComp], - providers: [ - A2uiRendererService, - BasicCatalog, - { - provide: A2UI_RENDERER_CONFIG, - useFactory: (basicCatalog: BasicCatalog) => ({ - catalogs: [basicCatalog], - }), - deps: [BasicCatalog], - }, - ], - }).compileComponents(); - - fixture = TestBed.createComponent(TestBasicComp); - rendererService = TestBed.inject(A2uiRendererService); - rendererService.processMessages([ - { - version: 'v0.9', - createSurface: { - surfaceId: 'test-surface', - catalogId: 'https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json', - }, - }, - ]); - }); - - it('should set --a2ui-color-primary style on host', () => { - const surface = rendererService.surfaceGroup.getSurface('test-surface'); - surface!.theme.primaryColor = '#00FF00'; - - fixture.componentRef.setInput('surfaceId', 'test-surface'); - fixture.detectChanges(); - - const element = fixture.nativeElement; - expect(element.style.getPropertyValue('--a2ui-color-primary')).toBe('#00FF00'); - }); - - it('should handle missing theme or primaryColor', () => { - fixture.componentRef.setInput('surfaceId', 'test-surface'); - fixture.detectChanges(); - - const element = fixture.nativeElement; - expect(element.style.getPropertyValue('--a2ui-color-primary')).toBe(''); - }); -}); diff --git a/renderers/angular/src/v0_9/catalog/basic/basic-catalog-component.ts b/renderers/angular/src/v0_9/catalog/basic/basic-catalog-component.ts deleted file mode 100644 index ce6cdaeafb..0000000000 --- a/renderers/angular/src/v0_9/catalog/basic/basic-catalog-component.ts +++ /dev/null @@ -1,78 +0,0 @@ -/** - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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 {Directive, computed, HostBinding, inject} from '@angular/core'; -import {injectBasicCatalogStyles} from '@a2ui/web_core/v0_9/basic_catalog'; -import {A2uiRendererService} from '../../core/a2ui-renderer.service'; -import {ComponentApi} from '@a2ui/web_core/v0_9'; -import {CatalogComponent} from '../../core/catalog_component'; -import {BoundProperty} from '../../core/types'; - -/** - * Base class for A2UI basic catalog components in Angular. - * - * Automatically injects the basic catalog styles when the component is instantiated. - * Also binds the primary brand color to the host element. - */ -@Directive() -export abstract class BasicCatalogComponent< - Api extends ComponentApi, -> extends CatalogComponent { - protected rendererService = inject(A2uiRendererService); - - readonly surface = computed(() => { - try { - const id = this.surfaceId(); - return id ? this.rendererService.surfaceGroup.getSurface(id) : undefined; - } catch { - return undefined; - } - }); - - readonly theme = computed(() => { - return this.surface()?.theme; - }); - - readonly primaryColor = computed(() => { - return this.theme()?.primaryColor; - }); - - /** - * Weight is applied as flex css property on the component host HTML element. - */ - protected readonly weight = computed(() => { - const props = this.props() as {weight?: BoundProperty}; - return props['weight']?.value() ?? null; - }); - - constructor() { - super(); - injectBasicCatalogStyles(); - } - - /** - * Binds the flex style to the host element based on the weight. - */ - @HostBinding('style.flex') - get flexStyle() { - return this.weight() !== null ? `${this.weight()}` : null; - } - - @HostBinding('style.--a2ui-color-primary') - get primaryColorStyle(): string | null { - return this.primaryColor() || null; - } -} diff --git a/renderers/angular/src/v0_9/catalog/basic/button.component.spec.ts b/renderers/angular/src/v0_9/catalog/basic/button.component.spec.ts deleted file mode 100644 index ad95120600..0000000000 --- a/renderers/angular/src/v0_9/catalog/basic/button.component.spec.ts +++ /dev/null @@ -1,170 +0,0 @@ -/** - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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 {ComponentFixture, TestBed} from '@angular/core/testing'; -import {Component, input} from '@angular/core'; -import {ButtonComponent} from './button.component'; -import {Action, ComponentModel} from '@a2ui/web_core/v0_9'; -import {A2uiRendererService} from '../../core/a2ui-renderer.service'; -import {ComponentBinder, Child} from '../../core/component-binder.service'; -import {By} from '@angular/platform-browser'; -import {setComponentProps, createBoundProperty, ComponentToProps} from '@a2ui/angular/testing'; - -describe('ButtonComponent', () => { - let component: ButtonComponent; - let fixture: ComponentFixture; - let mockRendererService: any; - let mockSurface: any; - let mockSurfaceGroup: any; - let defaultProps: ComponentToProps; - - beforeEach(async () => { - mockSurface = { - dispatchAction: jasmine.createSpy('dispatchAction'), - componentsModel: new Map([ - ['child1', new ComponentModel('child1', 'Text', {text: 'Child Content'})], - ]), - catalog: { - id: 'test-catalog', - components: new Map([ - [ - 'Text', - { - component: (() => { - @Component({ - standalone: true, - selector: 'dummy-text', - template: 'Dummy Text', - }) - class DummyText { - props = input(); - surfaceId = input(); - componentId = input(); - dataContextPath = input(); - } - return DummyText; - })(), - }, - ], - ]), - }, - }; - - mockSurfaceGroup = { - getSurface: jasmine.createSpy('getSurface').and.returnValue(mockSurface), - }; - - mockRendererService = { - surfaceGroup: mockSurfaceGroup, - }; - - const mockBinder = jasmine.createSpyObj('ComponentBinder', ['bind']); - mockBinder.bind.and.returnValue({text: {value: () => 'bound text'}}); - - await TestBed.configureTestingModule({ - imports: [ButtonComponent], - providers: [ - {provide: A2uiRendererService, useValue: mockRendererService}, - {provide: ComponentBinder, useValue: mockBinder}, - ], - }).compileComponents(); - - fixture = TestBed.createComponent(ButtonComponent); - component = fixture.componentInstance; - fixture.componentRef.setInput('surfaceId', 'surf1'); - fixture.componentRef.setInput('componentId', 'comp1'); - - defaultProps = { - variant: createBoundProperty('primary' as const), - child: createBoundProperty({id: 'child1', basePath: '/'}), - action: createBoundProperty({ - event: {name: 'test-action'}, - }), - isValid: createBoundProperty(true), - validationErrors: createBoundProperty([]), - }; - setComponentProps(fixture, defaultProps); - }); - - it('should create', () => { - fixture.detectChanges(); - expect(component).toBeTruthy(); - }); - - it('should set button type to submit for primary variant', () => { - fixture.detectChanges(); - const button = fixture.debugElement.query(By.css('button')); - expect(button.nativeElement.type).toBe('submit'); - }); - - it('should set button type to button for non-primary variant', () => { - setComponentProps(fixture, { - ...defaultProps, - variant: createBoundProperty('default' as const), - }); - fixture.detectChanges(); - const button = fixture.debugElement.query(By.css('button')); - expect(button.nativeElement.type).toBe('button'); - }); - - it('should apply variant class', () => { - fixture.detectChanges(); - const button = fixture.debugElement.query(By.css('button')); - expect(button.nativeElement.classList).toContain('primary'); - }); - - it('should handle click and dispatch action with sourceComponentId', () => { - fixture.detectChanges(); - const button = fixture.debugElement.query(By.css('button')); - button.triggerEventHandler('click', null); - - expect(mockSurfaceGroup.getSurface).toHaveBeenCalledWith('surf1'); - expect(mockSurface.dispatchAction).toHaveBeenCalledWith(jasmine.any(Object), 'comp1'); - }); - - it('should show child component host if child prop is present', () => { - fixture.detectChanges(); - const host = fixture.debugElement.query(By.css('a2ui-v09-component-host')); - expect(host).toBeTruthy(); - expect(host.componentInstance.componentKey()).toEqual({id: 'child1', basePath: '/'}); - }); - - it('should be disabled when isValid is false', () => { - const isValidProp = createBoundProperty(true); - - setComponentProps(fixture, { - ...defaultProps, - isValid: isValidProp, - }); - - fixture.detectChanges(); - const button = fixture.debugElement.query(By.css('button')); - expect(button.nativeElement.disabled).toBeFalse(); - - isValidProp.value.set(false); - fixture.detectChanges(); - expect(button.nativeElement.disabled).toBeTrue(); - }); - - it('should override the button default background color when primary color is set', () => { - mockSurface.theme = {primaryColor: 'red'}; - fixture.detectChanges(); - const button = fixture.debugElement.query(By.css('button')); - const computedStyle = window.getComputedStyle(button.nativeElement); - - expect(computedStyle.backgroundColor).toBe('rgb(255, 0, 0)'); // 'red' is evaluated to rgb in computed style - }); -}); diff --git a/renderers/angular/src/v0_9/catalog/basic/button.component.ts b/renderers/angular/src/v0_9/catalog/basic/button.component.ts deleted file mode 100644 index 849ab51dbf..0000000000 --- a/renderers/angular/src/v0_9/catalog/basic/button.component.ts +++ /dev/null @@ -1,114 +0,0 @@ -/** - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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, ChangeDetectionStrategy} from '@angular/core'; -import {ComponentHostComponent} from '../../core/component-host.component'; -import {DataContext} from '@a2ui/web_core/v0_9'; -import {BasicCatalogComponent} from './basic-catalog-component'; -import {ButtonApi} from '@a2ui/web_core/v0_9/basic_catalog'; - -/** - * Angular implementation of the A2UI Button component (v0.9). - * - * Renders a clickable button with a single child component (usually Text). - * Dispatches an action when clicked if an `action` property is provided. - * - * Supported CSS variables: - * - `--a2ui-button-padding`: Controls the padding. - * - `--a2ui-button-border-radius`: Controls the border radius. - * - `--a2ui-button-border`: Controls the border. - * - `--a2ui-button-margin`: Controls the margin. - * - `--a2ui-button-background`: Controls the background color. - * - `--a2ui-button-box-shadow`: Controls the box shadow. - * - `--a2ui-button-font-weight`: Controls the font weight. - */ -@Component({ - selector: 'a2ui-v09-button', - standalone: true, - imports: [ComponentHostComponent], - template: ` - - `, - styles: [ - ` - .a2ui-button { - padding: var( - --a2ui-button-padding, - var(--a2ui-spacing-m, 0.5rem) var(--a2ui-spacing-l, 1rem) - ); - border-radius: var(--a2ui-button-border-radius, var(--a2ui-spacing-s, 0.25rem)); - border: var( - --a2ui-button-border, - var(--a2ui-border-width, 1px) solid var(--a2ui-color-border, #ccc) - ); - cursor: pointer; - margin: var(--a2ui-button-margin, var(--a2ui-spacing-m, 0.5rem)); - background: var(--a2ui-button-background, var(--a2ui-color-surface, #fff)); - box-shadow: var(--a2ui-button-box-shadow, none); - font-weight: var(--a2ui-button-font-weight, normal); - --_a2ui-text-margin: 0; - --_a2ui-text-color: var(--a2ui-color-on-secondary, #333); - color: var(--_a2ui-text-color); - } - .a2ui-button.primary { - background: var(--a2ui-color-primary, #17e); - --_a2ui-text-color: var(--a2ui-color-on-primary, #fff); - color: var(--_a2ui-text-color); - border: none; - } - .a2ui-button.borderless { - background: none; - border: none; - padding: 0; - color: var(--a2ui-color-primary, #17e); - } - .a2ui-button:disabled { - background-color: #e9ecef; - color: #6c757d; - border-color: #ced4da; - cursor: not-allowed; - } - `, - ], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class ButtonComponent extends BasicCatalogComponent { - readonly variant = computed(() => this.props()['variant']?.value() ?? 'default'); - readonly child = computed(() => this.props()['child']?.value()); - readonly action = computed(() => this.props()['action']?.value()); - - handleClick() { - const action = this.action(); - if (action) { - const surface = this.surface(); - if (surface) { - const dataContext = new DataContext(surface, this.dataContextPath()); - const resolvedAction = dataContext.resolveAction(action); - surface.dispatchAction(resolvedAction, this.componentId()); - } - } - } -} diff --git a/renderers/angular/src/v0_9/catalog/basic/card.component.spec.ts b/renderers/angular/src/v0_9/catalog/basic/card.component.spec.ts deleted file mode 100644 index d216ddb53e..0000000000 --- a/renderers/angular/src/v0_9/catalog/basic/card.component.spec.ts +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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 {ComponentFixture, TestBed} from '@angular/core/testing'; -import {Component, input} from '@angular/core'; -import {By} from '@angular/platform-browser'; -import {CardComponent} from './card.component'; -import {ComponentModel} from '@a2ui/web_core/v0_9'; -import {A2uiRendererService} from '../../core/a2ui-renderer.service'; -import {ComponentBinder} from '../../core/component-binder.service'; -import {setComponentProps, createBoundProperty, ComponentToProps} from '@a2ui/angular/testing'; - -@Component({ - selector: 'dummy-text-for-card', - template: '
{{text}}
', - standalone: true, -}) -class DummyTextComponent { - text?: string; - props = input>(); - surfaceId = input(); - componentId = input(); - dataContextPath = input(); -} - -describe('CardComponent', () => { - let component: CardComponent; - let fixture: ComponentFixture; - let defaultProps: ComponentToProps; - - beforeEach(async () => { - const mockRendererService = { - surfaceGroup: { - getSurface: jasmine.createSpy('getSurface').and.returnValue({ - componentsModel: new Map([ - ['child-1', new ComponentModel('child-1', 'Text', {text: {value: 'Child 1'}})], - ]), - catalog: { - id: 'mock-catalog', - components: new Map([['Text', {type: 'Text', component: DummyTextComponent}]]), - }, - }), - }, - }; - const mockBinder = jasmine.createSpyObj('ComponentBinder', ['bind']); - - await TestBed.configureTestingModule({ - imports: [CardComponent], - providers: [ - {provide: A2uiRendererService, useValue: mockRendererService}, - {provide: ComponentBinder, useValue: mockBinder}, - ], - }).compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(CardComponent); - component = fixture.componentInstance; - fixture.componentRef.setInput('surfaceId', 'test-surface'); - fixture.componentRef.setInput('dataContextPath', '/'); - - defaultProps = { - child: createBoundProperty({id: 'child-1', basePath: '/'}), - }; - setComponentProps(fixture, defaultProps); - }); - - it('should create', () => { - fixture.detectChanges(); - expect(component).toBeTruthy(); - }); - - it('should render component-host for child', () => { - fixture.detectChanges(); - const host = fixture.debugElement.query(By.css('a2ui-v09-component-host')); - expect(host).toBeTruthy(); - expect(host.componentInstance.componentKey()).toEqual({id: 'child-1', basePath: '/'}); - }); -}); diff --git a/renderers/angular/src/v0_9/catalog/basic/card.component.ts b/renderers/angular/src/v0_9/catalog/basic/card.component.ts deleted file mode 100644 index ab891b2a7c..0000000000 --- a/renderers/angular/src/v0_9/catalog/basic/card.component.ts +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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, ChangeDetectionStrategy} from '@angular/core'; -import {ComponentHostComponent} from '../../core/component-host.component'; -import {BasicCatalogComponent} from './basic-catalog-component'; -import {CardApi} from '@a2ui/web_core/v0_9/basic_catalog'; - -/** - * Angular implementation of the A2UI Card component (v0.9). - * - * Renders a container with a shadow and rounded corners for grouping related content. - * - * Supported CSS variables: - * - `--a2ui-card-padding`: Controls the padding. - * - `--a2ui-card-border-radius`: Controls the border radius. - * - `--a2ui-card-box-shadow`: Controls the box shadow. - * - `--a2ui-card-background`: Controls the background color. - * - `--a2ui-card-border`: Controls the border. - * - `--a2ui-card-margin`: Controls the margin. - */ -@Component({ - selector: 'a2ui-v09-card', - standalone: true, - imports: [ComponentHostComponent], - template: ` -
- @if (child()) { - - - } -
- `, - styles: [ - ` - .a2ui-card { - padding: var(--a2ui-card-padding, var(--a2ui-spacing-m, 16px)); - border-radius: var(--a2ui-card-border-radius, var(--a2ui-border-radius, 8px)); - box-shadow: var(--a2ui-card-box-shadow, 0 2px 4px rgba(0, 0, 0, 0.1)); - background: var(--a2ui-card-background, var(--a2ui-color-surface, #fff)); - border: var( - --a2ui-card-border, - var(--a2ui-border-width, 1px) solid var(--a2ui-color-border, #ccc) - ); - margin: var(--a2ui-card-margin, var(--a2ui-spacing-m, 16px)); - } - `, - ], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class CardComponent extends BasicCatalogComponent { - readonly child = computed(() => this.props()['child']?.value()); -} diff --git a/renderers/angular/src/v0_9/catalog/basic/check-box.component.spec.ts b/renderers/angular/src/v0_9/catalog/basic/check-box.component.spec.ts deleted file mode 100644 index ad36affa73..0000000000 --- a/renderers/angular/src/v0_9/catalog/basic/check-box.component.spec.ts +++ /dev/null @@ -1,112 +0,0 @@ -/** - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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 {ComponentFixture, TestBed} from '@angular/core/testing'; -import {signal as angularSignal} from '@angular/core'; -import {CheckBoxComponent} from './check-box.component'; -import {A2uiRendererService} from '../../core/a2ui-renderer.service'; -import {ComponentBinder} from '../../core/component-binder.service'; -import {setComponentProps, createBoundProperty, ComponentToProps} from '@a2ui/angular/testing'; - -describe('CheckBoxComponent', () => { - let component: CheckBoxComponent; - let fixture: ComponentFixture; - let mockRendererService: {surfaceGroup: {getSurface: jasmine.Spy}}; - let defaultProps: ComponentToProps; - - beforeEach(async () => { - mockRendererService = { - surfaceGroup: { - getSurface: jasmine.createSpy('getSurface').and.returnValue({ - componentsModel: new Map(), - catalog: { - id: 'mock-catalog', - components: new Map(), - }, - }), - }, - }; - const mockBinder = jasmine.createSpyObj('ComponentBinder', ['bind']); - - await TestBed.configureTestingModule({ - imports: [CheckBoxComponent], - providers: [ - {provide: A2uiRendererService, useValue: mockRendererService}, - {provide: ComponentBinder, useValue: mockBinder}, - ], - }).compileComponents(); - - fixture = TestBed.createComponent(CheckBoxComponent); - component = fixture.componentInstance; - fixture.componentRef.setInput('surfaceId', 'test-surface'); - fixture.componentRef.setInput('dataContextPath', '/'); - - defaultProps = { - label: createBoundProperty(''), - value: createBoundProperty(false), - isValid: createBoundProperty(true), - validationErrors: createBoundProperty([]), - }; - setComponentProps(fixture, defaultProps); - }); - - it('should create', () => { - fixture.detectChanges(); - expect(component).toBeTruthy(); - }); - - it('should show label and checked state', () => { - setComponentProps(fixture, { - ...defaultProps, - label: createBoundProperty('Check me'), - value: createBoundProperty(true), - }); - fixture.detectChanges(); - const input = fixture.nativeElement.querySelector('input'); - expect(input.checked).toBe(true); - expect(fixture.nativeElement.textContent).toContain('Check me'); - }); - - it('should call onUpdate when toggled', () => { - const onUpdateSpy = jasmine.createSpy('onUpdate'); - setComponentProps(fixture, { - ...defaultProps, - value: {value: angularSignal(false), raw: false, onUpdate: onUpdateSpy}, - }); - fixture.detectChanges(); - const input = fixture.nativeElement.querySelector('input'); - input.click(); - expect(onUpdateSpy).toHaveBeenCalledWith(true); - }); - - it('should apply primary color when checked', () => { - setComponentProps(fixture, { - ...defaultProps, - value: createBoundProperty(true), - }); - mockRendererService.surfaceGroup.getSurface.and.returnValue({ - theme: {primaryColor: 'rgb(255, 0, 0)'}, - componentsModel: new Map(), - catalog: {components: new Map()}, - }); - fixture.detectChanges(); - - const input = fixture.nativeElement.querySelector('input'); - const styles = window.getComputedStyle(input); - - expect(styles.accentColor).toBe('rgb(255, 0, 0)'); - }); -}); diff --git a/renderers/angular/src/v0_9/catalog/basic/check-box.component.ts b/renderers/angular/src/v0_9/catalog/basic/check-box.component.ts deleted file mode 100644 index c864a61272..0000000000 --- a/renderers/angular/src/v0_9/catalog/basic/check-box.component.ts +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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, ChangeDetectionStrategy} from '@angular/core'; -import {BasicCatalogComponent} from './basic-catalog-component'; -import {CheckBoxApi} from '@a2ui/web_core/v0_9/basic_catalog'; - -/** - * Angular implementation of the A2UI CheckBox component (v0.9). - * - * Renders a checkbox with a label. Updates the bound data model property - * when the checked state changes. - * - * Supported CSS variables: - * - `--a2ui-checkbox-margin`: Controls the margin. - * - `--a2ui-checkbox-gap`: Controls the gap between checkbox and label. - * - `--a2ui-checkbox-label-font-size`: Controls the font size of the label. - * - `--a2ui-checkbox-label-font-weight`: Controls the font weight of the label. - * - `--a2ui-checkbox-size`: Controls the width and height of the checkbox. - * - `--a2ui-checkbox-background`: Controls the background of the checkbox. - * - `--a2ui-checkbox-border`: Controls the border of the checkbox. - * - `--a2ui-checkbox-border-radius`: Controls the border radius of the checkbox. - * - `--a2ui-checkbox-color-error`: Controls the color for error states. - */ -@Component({ - selector: 'a2ui-v09-check-box', - standalone: true, - imports: [], - template: ` - - `, - styles: [ - ` - .a2ui-check-box-label { - display: flex; - align-items: center; - gap: var(--a2ui-checkbox-gap, var(--a2ui-spacing-s, 0.5rem)); - cursor: pointer; - padding: 4px 0; - margin: var(--a2ui-checkbox-margin, var(--a2ui-spacing-m, 16px)); - color: var(--a2ui-text-color-text, var(--a2ui-color-on-background, #333)); - } - .a2ui-check-box-input { - width: var(--a2ui-checkbox-size, 1rem); - height: var(--a2ui-checkbox-size, 1rem); - cursor: pointer; - background: var(--a2ui-checkbox-background, inherit); - border: var(--a2ui-checkbox-border, var(--a2ui-border-width, 1px) solid #ccc); - border-radius: var(--a2ui-checkbox-border-radius, 4px); - accent-color: var(--a2ui-color-primary); - } - .a2ui-check-box-text { - font-size: var( - --a2ui-checkbox-label-font-size, - var(--a2ui-label-font-size, var(--a2ui-font-size-s, 16px)) - ); - font-weight: var(--a2ui-checkbox-label-font-weight, bold); - } - `, - ], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class CheckBoxComponent extends BasicCatalogComponent { - readonly value = computed(() => this.props()['value']?.value() === true); - readonly label = computed(() => this.props()['label']?.value()); - - handleChange(event: Event) { - const checked = (event.target as HTMLInputElement).checked; - this.props()['value']?.onUpdate(checked); - } -} diff --git a/renderers/angular/src/v0_9/catalog/basic/choice-picker.component.spec.ts b/renderers/angular/src/v0_9/catalog/basic/choice-picker.component.spec.ts deleted file mode 100644 index cf95ba31fd..0000000000 --- a/renderers/angular/src/v0_9/catalog/basic/choice-picker.component.spec.ts +++ /dev/null @@ -1,136 +0,0 @@ -/** - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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 {ComponentFixture, TestBed} from '@angular/core/testing'; -import {signal as angularSignal} from '@angular/core'; -import {ChoicePickerComponent} from './choice-picker.component'; -import {DynamicString} from '@a2ui/web_core/v0_9'; -import {A2uiRendererService} from '../../core/a2ui-renderer.service'; -import {ComponentBinder} from '../../core/component-binder.service'; -import {setComponentProps, createBoundProperty, ComponentToProps} from '@a2ui/angular/testing'; - -describe('ChoicePickerComponent', () => { - let component: ChoicePickerComponent; - let fixture: ComponentFixture; - let defaultProps: ComponentToProps; - - beforeEach(async () => { - const mockRendererService = { - surfaceGroup: { - getSurface: jasmine.createSpy('getSurface').and.returnValue({ - componentsModel: new Map(), - catalog: { - id: 'mock-catalog', - components: new Map(), - }, - }), - }, - }; - const mockBinder = jasmine.createSpyObj('ComponentBinder', ['bind']); - - await TestBed.configureTestingModule({ - imports: [ChoicePickerComponent], - providers: [ - {provide: A2uiRendererService, useValue: mockRendererService}, - {provide: ComponentBinder, useValue: mockBinder}, - ], - }).compileComponents(); - - fixture = TestBed.createComponent(ChoicePickerComponent); - component = fixture.componentInstance; - fixture.componentRef.setInput('surfaceId', 'test-surface'); - fixture.componentRef.setInput('componentId', 'test-choice-picker'); - fixture.componentRef.setInput('dataContextPath', '/'); - - defaultProps = { - label: createBoundProperty(''), - options: createBoundProperty<{label: DynamicString; value: string}[]>([]), - value: createBoundProperty([]), - variant: createBoundProperty<'multipleSelection' | 'mutuallyExclusive' | undefined>( - 'mutuallyExclusive', - ), - displayStyle: createBoundProperty<'checkbox' | 'chips' | undefined>('checkbox'), - isValid: createBoundProperty(true), - validationErrors: createBoundProperty([]), - }; - setComponentProps(fixture, defaultProps); - }); - - it('should create', () => { - fixture.detectChanges(); - expect(component).toBeTruthy(); - }); - - it('should render options', () => { - setComponentProps(fixture, { - ...defaultProps, - label: createBoundProperty('Pick one'), - options: createBoundProperty<{label: DynamicString; value: string}[]>([ - {label: 'Opt 1', value: '1'}, - {label: 'Opt 2', value: '2'}, - ]), - value: createBoundProperty(['1']), - }); - fixture.detectChanges(); - const options = fixture.nativeElement.querySelectorAll('.a2ui-option-label'); - expect(options.length).toBe(2); - expect(options[0].textContent).toContain('Opt 1'); - }); - - it('should call onUpdate when option selected', () => { - const onUpdateSpy = jasmine.createSpy('onUpdate'); - setComponentProps(fixture, { - ...defaultProps, - label: createBoundProperty('Pick one'), - options: createBoundProperty<{label: DynamicString; value: string}[]>([ - {label: 'Opt 1', value: '1'}, - {label: 'Opt 2', value: '2'}, - ]), - value: {value: angularSignal(['1']), raw: ['1'], onUpdate: onUpdateSpy}, - }); - fixture.detectChanges(); - const opt2Input = fixture.nativeElement.querySelector('input[value="2"]'); - opt2Input.click(); - expect(onUpdateSpy).toHaveBeenCalledWith(['2']); - }); - - it('should render chips and toggle selection', () => { - const onUpdateSpy = jasmine.createSpy('onUpdate'); - setComponentProps(fixture, { - ...defaultProps, - options: createBoundProperty<{label: DynamicString; value: string}[]>([ - {label: 'Chip 1', value: 'c1'}, - {label: 'Chip 2', value: 'c2'}, - ]), - value: {value: angularSignal(['c1']), raw: ['c1'], onUpdate: onUpdateSpy}, - variant: createBoundProperty<'multipleSelection' | 'mutuallyExclusive' | undefined>( - 'multipleSelection', - ), - displayStyle: createBoundProperty<'checkbox' | 'chips' | undefined>('chips'), - }); - fixture.detectChanges(); - const chips = fixture.nativeElement.querySelectorAll('.a2ui-chip'); - expect(chips.length).toBe(2); - expect(chips[0].classList.contains('active')).toBeTrue(); - expect(chips[1].classList.contains('active')).toBeFalse(); - - chips[1].click(); - expect(onUpdateSpy).toHaveBeenCalledWith(['c1', 'c2']); - - chips[0].click(); - expect(onUpdateSpy).toHaveBeenCalledWith([]); - }); -}); diff --git a/renderers/angular/src/v0_9/catalog/basic/choice-picker.component.ts b/renderers/angular/src/v0_9/catalog/basic/choice-picker.component.ts deleted file mode 100644 index dd7280470b..0000000000 --- a/renderers/angular/src/v0_9/catalog/basic/choice-picker.component.ts +++ /dev/null @@ -1,173 +0,0 @@ -/** - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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, ChangeDetectionStrategy} from '@angular/core'; -import {BasicCatalogComponent} from './basic-catalog-component'; -import {ChoicePickerApi} from '@a2ui/web_core/v0_9/basic_catalog'; - -/** - * Angular implementation of the A2UI ChoicePicker component (v0.9). - * - * Renders a set of options as either radio buttons/checkboxes or chips. - * Supports both single and multiple selection. - * - * Supported CSS variables: - * - `--a2ui-choicepicker-gap`: Controls spacing between options/chips. - * - `--a2ui-choicepicker-padding`: Controls padding of the container. - * - `--a2ui-choicepicker-checkbox-size`: Controls size of checkboxes/radios. - * - `--a2ui-choicepicker-chip-padding`: Controls padding of chips. - * - `--a2ui-choicepicker-chip-border-radius`: Controls border radius of chips. - * - `--a2ui-choicepicker-chip-border`: Controls border of chips. - * - `--a2ui-choicepicker-chip-background`: Controls background of chips. - * - `--a2ui-choicepicker-chip-background-selected`: Controls background of selected chips. - */ -@Component({ - selector: 'a2ui-v09-choice-picker', - standalone: true, - imports: [], - template: ` -
- - @if (displayStyle() === 'chips') { -
- @for (option of options(); track option.value) { - - } -
- } @else { - -
- @for (option of options(); track option.value) { - - } -
- } -
- `, - styles: [ - ` - .a2ui-choice-picker { - width: 100%; - padding: var(--a2ui-choicepicker-padding, 0); - } - .a2ui-options-group { - display: flex; - flex-direction: column; - gap: var(--a2ui-choicepicker-gap, var(--a2ui-spacing-xs, 0.25rem)); - } - .a2ui-option-label { - display: flex; - align-items: center; - gap: var(--a2ui-choicepicker-gap, var(--a2ui-spacing-xs, 0.25rem)); - cursor: pointer; - color: var(--a2ui-text-color-text, var(--a2ui-color-on-background, #333)); - } - .a2ui-option-input { - width: var(--a2ui-choicepicker-checkbox-size, 1rem); - height: var(--a2ui-choicepicker-checkbox-size, 1rem); - } - .a2ui-chips-group { - display: flex; - flex-wrap: wrap; - gap: var(--a2ui-choicepicker-gap, var(--a2ui-spacing-xs, 0.25rem)); - } - .a2ui-chip { - padding: var( - --a2ui-choicepicker-chip-padding, - var(--a2ui-spacing-s, 0.5rem) var(--a2ui-spacing-m, 1rem) - ); - border-radius: var(--a2ui-choicepicker-chip-border-radius, 100px); - border: var(--a2ui-choicepicker-chip-border, 1px solid var(--a2ui-color-border, #ccc)); - background: var(--a2ui-choicepicker-chip-background, var(--a2ui-color-surface, #fff)); - cursor: pointer; - transition: all 0.2s; - } - .a2ui-chip.active { - background-color: var( - --a2ui-choicepicker-chip-background-selected, - var(--a2ui-color-primary, #17e) - ); - color: var(--a2ui-color-on-primary, #fff); - border-color: var( - --a2ui-choicepicker-chip-background-selected, - var(--a2ui-color-primary, #17e) - ); - } - `, - ], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class ChoicePickerComponent extends BasicCatalogComponent { - readonly displayStyle = computed(() => this.props()['displayStyle']?.value()); - readonly options = computed(() => this.props()['options']?.value() || []); - readonly variant = computed(() => this.props()['variant']?.value()); - readonly selectedValue = computed(() => this.props()['value']?.value()); - - isMultiple(): boolean { - return this.variant() === 'multipleSelection'; - } - - isSelected(value: string): boolean { - const selected = this.selectedValue(); - if (Array.isArray(selected)) { - return selected.includes(value); - } - return selected === value; - } - - onCheckChange(value: string, event: Event) { - const checked = (event.target as HTMLInputElement).checked; - this.updateValue(value, checked); - } - - toggleActive(value: string) { - const current = this.isSelected(value); - this.updateValue(value, !current); - } - - private updateValue(value: string, active: boolean) { - const current = this.selectedValue(); - if (this.isMultiple()) { - let next = Array.isArray(current) ? [...current] : []; - if (active) { - if (!next.includes(value)) next.push(value); - } else { - next = next.filter((v: any) => v !== value); - } - this.props()['value']?.onUpdate(next); - } else { - if (active) { - this.props()['value']?.onUpdate([value]); - } - } - } -} diff --git a/renderers/angular/src/v0_9/catalog/basic/column.component.spec.ts b/renderers/angular/src/v0_9/catalog/basic/column.component.spec.ts deleted file mode 100644 index 19a9f7f28a..0000000000 --- a/renderers/angular/src/v0_9/catalog/basic/column.component.spec.ts +++ /dev/null @@ -1,187 +0,0 @@ -/** - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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 {ComponentFixture, TestBed} from '@angular/core/testing'; -import {Component, input, signal} from '@angular/core'; -import {ColumnComponent} from './column.component'; -import {ComponentModel} from '@a2ui/web_core/v0_9'; -import {A2uiRendererService} from '../../core/a2ui-renderer.service'; -import {ComponentBinder} from '../../core/component-binder.service'; -import {By} from '@angular/platform-browser'; -import {setComponentProps, createBoundProperty, ComponentToProps} from '@a2ui/angular/testing'; - -@Component({ - standalone: true, - selector: 'dummy-child', - template: 'Dummy Child', -}) -class DummyChild { - props = input(); - surfaceId = input(); - componentId = input(); - dataContextPath = input(); -} - -describe('ColumnComponent', () => { - let component: ColumnComponent; - let fixture: ComponentFixture; - let mockRendererService: any; - let mockSurface: any; - let mockSurfaceGroup: any; - let mockBinder: any; - let defaultProps: ComponentToProps; - - beforeEach(async () => { - mockSurface = { - componentsModel: new Map([ - ['child1', new ComponentModel('child1', 'Child', {})], - ['child2', new ComponentModel('child2', 'Child', {})], - ['template1', new ComponentModel('template1', 'Child', {})], - ]), - catalog: { - id: 'test-catalog', - components: new Map([['Child', {component: DummyChild}]]), - }, - }; - - mockSurfaceGroup = { - getSurface: jasmine.createSpy('getSurface').and.returnValue(mockSurface), - }; - - mockRendererService = { - surfaceGroup: mockSurfaceGroup, - }; - - mockBinder = jasmine.createSpyObj('ComponentBinder', ['bind']); - mockBinder.bind.and.returnValue({text: {value: () => 'bound'}}); - - await TestBed.configureTestingModule({ - imports: [ColumnComponent], - providers: [ - {provide: A2uiRendererService, useValue: mockRendererService}, - {provide: ComponentBinder, useValue: mockBinder}, - ], - }).compileComponents(); - - fixture = TestBed.createComponent(ColumnComponent); - component = fixture.componentInstance; - fixture.componentRef.setInput('surfaceId', 'surf1'); - - defaultProps = { - justify: createBoundProperty('start' as const), - align: createBoundProperty('stretch' as const), - children: createBoundProperty([ - {id: 'child1', basePath: '/'}, - {id: 'child2', basePath: '/'}, - ]), - }; - setComponentProps(fixture, defaultProps); - }); - - it('should create', () => { - fixture.detectChanges(); - expect(component).toBeTruthy(); - }); - - it('should apply flex styles from props', () => { - fixture.detectChanges(); - const style = window.getComputedStyle(fixture.debugElement.nativeElement); - expect(style.justifyContent).toBe('flex-start'); - expect(style.alignItems).toBe('stretch'); - }); - - it('should apply flex style from weight prop', () => { - setComponentProps(fixture, { - ...defaultProps, - weight: createBoundProperty(2), - }); - fixture.detectChanges(); - const style = window.getComputedStyle(fixture.debugElement.nativeElement); - expect(style.flex).toBe('2 1 0%'); - }); - - it('should apply flex style from weight prop when value is 0', () => { - setComponentProps(fixture, { - ...defaultProps, - weight: createBoundProperty(0), - }); - fixture.detectChanges(); - const style = window.getComputedStyle(fixture.debugElement.nativeElement); - expect(style.flex).toBe('0 1 0%'); - }); - - it('should not apply flex style when weight prop is null', () => { - setComponentProps(fixture, { - ...defaultProps, - weight: createBoundProperty(undefined), - }); - fixture.detectChanges(); - const style = window.getComputedStyle(fixture.debugElement.nativeElement); - expect(style.flex).not.toBe('2 1 0%'); - expect(style.flex).not.toBe('0 1 0%'); - }); - - it('should render non-repeating children', () => { - fixture.detectChanges(); - const hosts = fixture.debugElement.queryAll(By.css('a2ui-v09-component-host')); - expect(hosts.length).toBe(2); - expect(hosts[0].componentInstance.componentKey()).toEqual({id: 'child1', basePath: '/'}); - expect(hosts[1].componentInstance.componentKey()).toEqual({id: 'child2', basePath: '/'}); - }); - - it('should render repeating children', () => { - setComponentProps(fixture, { - ...defaultProps, - children: { - value: signal([ - {id: 'template1', basePath: '/items/0'}, - {id: 'template1', basePath: '/items/1'}, - ]), - raw: { - componentId: 'template1', - path: 'items', - }, - template: { - id: 'template1', - path: 'items', - }, - onUpdate: jasmine.createSpy('onUpdate'), - }, - }); - fixture.detectChanges(); - - const hosts = fixture.debugElement.queryAll(By.css('a2ui-v09-component-host')); - expect(hosts.length).toBe(2); - expect(hosts[0].componentInstance.componentKey()).toEqual({ - id: 'template1', - basePath: '/items/0', - }); - expect(hosts[1].componentInstance.componentKey()).toEqual({ - id: 'template1', - basePath: '/items/1', - }); - }); - - it('should handle missing justify and align properties', () => { - setComponentProps(fixture, { - children: createBoundProperty([{id: 'child1', basePath: '/'}]), - }); - fixture.detectChanges(); - const div = fixture.debugElement; - expect(div.styles['justify-content']).toBeFalsy(); - expect(div.styles['align-items']).toBeFalsy(); - }); -}); diff --git a/renderers/angular/src/v0_9/catalog/basic/column.component.ts b/renderers/angular/src/v0_9/catalog/basic/column.component.ts deleted file mode 100644 index 98db94e6c7..0000000000 --- a/renderers/angular/src/v0_9/catalog/basic/column.component.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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, ChangeDetectionStrategy} from '@angular/core'; -import {ComponentHostComponent} from '../../core/component-host.component'; -import {Child} from '../../core/component-binder.service'; -import {BasicCatalogComponent} from './basic-catalog-component'; -import {JUSTIFY_MAP, ALIGN_MAP} from './utils'; -import {ColumnApi} from '@a2ui/web_core/v0_9/basic_catalog'; - -/** - * Angular implementation of the A2UI Column component (v0.9). - * - * Arranges child components in a vertical flex layout. Supports both static - * lists of children and repeating templates bound to a data collection. - * - * Supported CSS variables: - * - `--a2ui-column-gap`: Controls the gap between items in the column. Defaults to `--a2ui-spacing-m` (16px). - */ -@Component({ - selector: 'a2ui-v09-column', - standalone: true, - imports: [ComponentHostComponent], - host: { - '[style.display]': '"flex"', - '[style.flex-direction]': '"column"', - '[style.width]': '"100%"', - '[style.gap]': '"var(--a2ui-column-gap, var(--a2ui-spacing-m, 16px))"', - '[style.justify-content]': 'justify()', - '[style.align-items]': 'align()', - }, - template: ` - @for (child of children(); track trackChild($index, child)) { - - - } - `, - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class ColumnComponent extends BasicCatalogComponent { - protected readonly justify = computed(() => { - const val = this.props()['justify']?.value(); - return val ? JUSTIFY_MAP[val] || val : undefined; - }); - protected readonly align = computed(() => { - const val = this.props()['align']?.value(); - return val ? ALIGN_MAP[val] || val : undefined; - }); - - protected readonly children = computed(() => this.props()['children'].value() || []); - - protected trackChild(_index: number, child: Child) { - return `${child.basePath}/${child.id}`; - } -} diff --git a/renderers/angular/src/v0_9/catalog/basic/date-time-input.component.spec.ts b/renderers/angular/src/v0_9/catalog/basic/date-time-input.component.spec.ts deleted file mode 100644 index 12d7e96d80..0000000000 --- a/renderers/angular/src/v0_9/catalog/basic/date-time-input.component.spec.ts +++ /dev/null @@ -1,122 +0,0 @@ -/** - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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 {ComponentFixture, TestBed} from '@angular/core/testing'; -import {signal as angularSignal} from '@angular/core'; -import {DateTimeInputComponent} from './date-time-input.component'; -import {A2uiRendererService} from '../../core/a2ui-renderer.service'; -import {ComponentBinder} from '../../core/component-binder.service'; -import {setComponentProps, createBoundProperty, ComponentToProps} from '@a2ui/angular/testing'; - -describe('DateTimeInputComponent', () => { - let component: DateTimeInputComponent; - let fixture: ComponentFixture; - let defaultProps: ComponentToProps; - - beforeEach(async () => { - const mockRendererService = { - surfaceGroup: { - getSurface: jasmine.createSpy('getSurface').and.returnValue({ - componentsModel: new Map(), - catalog: { - id: 'mock-catalog', - components: new Map(), - }, - }), - }, - }; - const mockBinder = jasmine.createSpyObj('ComponentBinder', ['bind']); - - await TestBed.configureTestingModule({ - imports: [DateTimeInputComponent], - providers: [ - {provide: A2uiRendererService, useValue: mockRendererService}, - {provide: ComponentBinder, useValue: mockBinder}, - ], - }).compileComponents(); - - fixture = TestBed.createComponent(DateTimeInputComponent); - component = fixture.componentInstance; - fixture.componentRef.setInput('surfaceId', 'test-surface'); - fixture.componentRef.setInput('dataContextPath', '/'); - - defaultProps = { - label: createBoundProperty(''), - value: createBoundProperty(''), - enableDate: createBoundProperty(true), - enableTime: createBoundProperty(true), - isValid: createBoundProperty(true), - validationErrors: createBoundProperty([]), - }; - setComponentProps(fixture, defaultProps); - }); - - it('should create', () => { - fixture.detectChanges(); - expect(component).toBeTruthy(); - }); - - it('should render date input', () => { - setComponentProps(fixture, { - ...defaultProps, - label: createBoundProperty('Start Date'), - value: createBoundProperty('2026-03-16'), - enableTime: createBoundProperty(false), - }); - fixture.detectChanges(); - const input = fixture.nativeElement.querySelector('input[type="date"]'); - expect(input).toBeTruthy(); - expect(input.value).toBe('2026-03-16'); - }); - - it('should call onUpdate when date or time changes', () => { - const onUpdateSpy = jasmine.createSpy('onUpdate'); - setComponentProps(fixture, { - ...defaultProps, - value: { - value: angularSignal('2026-03-16T10:00:00'), - raw: '2026-03-16T10:00:00', - onUpdate: onUpdateSpy, - }, - }); - fixture.detectChanges(); - const dateInput = fixture.nativeElement.querySelector('input[type="date"]'); - const timeInput = fixture.nativeElement.querySelector('input[type="time"]'); - - dateInput.value = '2026-03-17'; - dateInput.dispatchEvent(new Event('change')); - expect(onUpdateSpy).toHaveBeenCalledWith('2026-03-17T10:00:00'); - - onUpdateSpy.calls.reset(); - timeInput.value = '11:00'; - timeInput.dispatchEvent(new Event('change')); - expect(onUpdateSpy).toHaveBeenCalledWith('2026-03-16T11:00:00'); - }); - - it('should handle empty value by returning empty strings', () => { - setComponentProps(fixture, { - ...defaultProps, - value: createBoundProperty(''), - }); - fixture.detectChanges(); - - const dateInput = fixture.nativeElement.querySelector('input[type="date"]'); - const timeInput = fixture.nativeElement.querySelector('input[type="time"]'); - - expect(dateInput.value).toBe(''); - expect(timeInput.value).toBe(''); - }); -}); diff --git a/renderers/angular/src/v0_9/catalog/basic/date-time-input.component.ts b/renderers/angular/src/v0_9/catalog/basic/date-time-input.component.ts deleted file mode 100644 index 3bfc81cd34..0000000000 --- a/renderers/angular/src/v0_9/catalog/basic/date-time-input.component.ts +++ /dev/null @@ -1,143 +0,0 @@ -/** - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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, ChangeDetectionStrategy} from '@angular/core'; -import {BasicCatalogComponent} from './basic-catalog-component'; -import {DateTimeInputApi} from '@a2ui/web_core/v0_9/basic_catalog'; - -/** - * Angular implementation of the A2UI DateTimeInput component (v0.9). - * - * Renders date and/or time input fields. Combines them into an ISO string - * for the bound data model property. - * - * Supported CSS variables: - * - `--a2ui-datetimeinput-background`: Controls the background of inputs. - * - `--a2ui-datetimeinput-color`: Controls the text color of inputs. - * - `--a2ui-datetimeinput-border`: Controls the border of inputs. - * - `--a2ui-datetimeinput-border-radius`: Controls the border radius of inputs. - * - `--a2ui-datetimeinput-padding`: Controls the padding of inputs. - * - `--a2ui-datetimeinput-label-font-size`: Controls the font size of the label. - * - `--a2ui-datetimeinput-label-font-weight`: Controls the font weight of the label. - */ -@Component({ - selector: 'a2ui-v09-date-time-input', - standalone: true, - imports: [], - template: ` -
- @if (label()) { - - } -
- @if (enableDate()) { - - } - @if (enableTime()) { - - } -
-
- `, - styles: [ - ` - .a2ui-date-time-container { - display: flex; - flex-direction: column; - gap: var(--a2ui-spacing-xs, 4px); - width: 100%; - } - .a2ui-date-time-label { - font-size: var( - --a2ui-datetimeinput-label-font-size, - var(--a2ui-label-font-size, var(--a2ui-font-size-s, 14px)) - ); - font-weight: var(--a2ui-datetimeinput-label-font-weight, bold); - color: var(--a2ui-text-color-text, var(--a2ui-color-on-background, #333)); - } - .a2ui-date-time-inputs { - display: flex; - gap: var(--a2ui-spacing-s, 8px); - width: 100%; - } - .a2ui-date-time-input { - padding: var(--a2ui-datetimeinput-padding, 8px); - border-radius: var(--a2ui-datetimeinput-border-radius, 4px); - border: var(--a2ui-datetimeinput-border, 1px solid var(--a2ui-color-border, #ccc)); - background-color: var(--a2ui-datetimeinput-background, var(--a2ui-color-input, #fff)); - color: var(--a2ui-datetimeinput-color, var(--a2ui-color-on-input, #333)); - font-family: inherit; - flex: 1; - } - .a2ui-date-time-input::-webkit-datetime-edit, - .a2ui-date-time-input::-webkit-datetime-edit-fields-wrapper { - color: var(--a2ui-datetimeinput-color, var(--a2ui-color-on-input, #333)); - } - `, - ], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class DateTimeInputComponent extends BasicCatalogComponent { - readonly label = computed(() => this.props()['label']?.value()); - readonly enableDate = computed(() => this.props()['enableDate']?.value() ?? true); - readonly enableTime = computed(() => this.props()['enableTime']?.value() ?? false); - - private readonly rawValue = computed(() => this.props()['value']?.value() || ''); - - readonly dateValue = computed(() => { - const val = this.rawValue(); - if (!val) return ''; - return val.includes('T') ? val.split('T')[0] : val; - }); - - readonly timeValue = computed(() => { - const val = this.rawValue(); - if (!val || !val.includes('T')) return ''; - return val.split('T')[1].substring(0, 5); - }); - - handleDateChange(event: Event) { - const date = (event.target as HTMLInputElement).value; - const current = this.rawValue(); - if (this.enableTime()) { - const time = current.includes('T') ? current.split('T')[1] : '00:00:00'; - this.props()['value']?.onUpdate(`${date}T${time}`); - } else { - this.props()['value']?.onUpdate(date); - } - } - - handleTimeChange(event: Event) { - const time = (event.target as HTMLInputElement).value; - const current = this.rawValue(); - const date = current.includes('T') - ? current.split('T')[0] - : current || new Date().toISOString().split('T')[0]; - this.props()['value']?.onUpdate(`${date}T${time}:00`); - } -} diff --git a/renderers/angular/src/v0_9/catalog/basic/divider.component.spec.ts b/renderers/angular/src/v0_9/catalog/basic/divider.component.spec.ts deleted file mode 100644 index 292bac4996..0000000000 --- a/renderers/angular/src/v0_9/catalog/basic/divider.component.spec.ts +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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 {ComponentFixture, TestBed} from '@angular/core/testing'; -import {DividerComponent} from './divider.component'; -import {A2uiRendererService} from '../../core/a2ui-renderer.service'; -import {ComponentBinder} from '../../core/component-binder.service'; -import {setComponentProps, createBoundProperty, ComponentToProps} from '@a2ui/angular/testing'; - -describe('DividerComponent', () => { - let component: DividerComponent; - let fixture: ComponentFixture; - let defaultProps: ComponentToProps; - - beforeEach(async () => { - const mockRendererService = { - surfaceGroup: { - getSurface: jasmine.createSpy('getSurface').and.returnValue({ - componentsModel: new Map(), - catalog: { - id: 'mock-catalog', - components: new Map(), - }, - }), - }, - }; - const mockBinder = jasmine.createSpyObj('ComponentBinder', ['bind']); - - await TestBed.configureTestingModule({ - imports: [DividerComponent], - providers: [ - {provide: A2uiRendererService, useValue: mockRendererService}, - {provide: ComponentBinder, useValue: mockBinder}, - ], - }).compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(DividerComponent); - component = fixture.componentInstance; - fixture.componentRef.setInput('surfaceId', 'test-surface'); - fixture.componentRef.setInput('dataContextPath', '/'); - - defaultProps = { - axis: createBoundProperty('horizontal' as const), - }; - setComponentProps(fixture, defaultProps); - }); - - it('should create', () => { - fixture.detectChanges(); - expect(component).toBeTruthy(); - }); - - it('should render horizontal divider by default', () => { - fixture.detectChanges(); - const divider = fixture.nativeElement.querySelector('.a2ui-divider'); - expect(divider.classList).toContain('horizontal'); - }); - - it('should render vertical divider', () => { - setComponentProps(fixture, { - ...defaultProps, - axis: createBoundProperty('vertical' as const), - }); - fixture.detectChanges(); - const divider = fixture.nativeElement.querySelector('.a2ui-divider'); - expect(divider.classList).toContain('vertical'); - }); -}); diff --git a/renderers/angular/src/v0_9/catalog/basic/divider.component.ts b/renderers/angular/src/v0_9/catalog/basic/divider.component.ts deleted file mode 100644 index 106b1ad859..0000000000 --- a/renderers/angular/src/v0_9/catalog/basic/divider.component.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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, ChangeDetectionStrategy} from '@angular/core'; -import {BasicCatalogComponent} from './basic-catalog-component'; -import {DividerApi} from '@a2ui/web_core/v0_9/basic_catalog'; - -/** - * Angular implementation of the A2UI Divider component (v0.9). - * - * Renders a horizontal or vertical line to separate content. - * - * Supported CSS variables: - * - `--a2ui-divider-border`: Controls the border of the divider (horizontal and vertical). - * - `--a2ui-divider-spacing`: Controls the margin around the divider. - */ -@Component({ - selector: 'a2ui-v09-divider', - standalone: true, - imports: [], - host: { - '[style.display]': '"block"', - '[style.width]': 'axis() === "horizontal" ? "100%" : "auto"', - }, - template: ` -
- `, - styles: [ - ` - .a2ui-divider { - border: 0; - border-top: var( - --a2ui-divider-border, - var(--a2ui-border-width, 1px) solid var(--a2ui-color-border, #ccc) - ); - margin: var(--a2ui-divider-spacing, var(--a2ui-spacing-m, 16px)) 0; - width: 100%; - } - .a2ui-divider.vertical { - width: var(--a2ui-border-width, 1px); - height: 100%; - margin: 0 var(--a2ui-divider-spacing, var(--a2ui-spacing-m, 16px)); - border-top: 0; - border-left: var( - --a2ui-divider-border, - var(--a2ui-border-width, 1px) solid var(--a2ui-color-border, #ccc) - ); - } - `, - ], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class DividerComponent extends BasicCatalogComponent { - readonly axis = computed(() => this.props()['axis']?.value() ?? 'horizontal'); -} diff --git a/renderers/angular/src/v0_9/catalog/basic/icon.component.spec.ts b/renderers/angular/src/v0_9/catalog/basic/icon.component.spec.ts deleted file mode 100644 index f93fe60c37..0000000000 --- a/renderers/angular/src/v0_9/catalog/basic/icon.component.spec.ts +++ /dev/null @@ -1,135 +0,0 @@ -/** - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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 {ComponentFixture, TestBed} from '@angular/core/testing'; -import {IconComponent} from './icon.component'; -import {A2uiRendererService} from '../../core/a2ui-renderer.service'; -import {ComponentBinder} from '../../core/component-binder.service'; -import {setComponentProps, createBoundProperty, ComponentToProps} from '@a2ui/angular/testing'; - -describe('IconComponent', () => { - let component: IconComponent; - let fixture: ComponentFixture; - let defaultProps: ComponentToProps; - - beforeEach(async () => { - const mockRendererService = { - surfaceGroup: { - getSurface: jasmine.createSpy('getSurface').and.returnValue({ - componentsModel: new Map(), - catalog: { - id: 'mock-catalog', - components: new Map(), - }, - }), - }, - }; - const mockBinder = jasmine.createSpyObj('ComponentBinder', ['bind']); - - await TestBed.configureTestingModule({ - imports: [IconComponent], - providers: [ - {provide: A2uiRendererService, useValue: mockRendererService}, - {provide: ComponentBinder, useValue: mockBinder}, - ], - }).compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(IconComponent); - component = fixture.componentInstance; - fixture.componentRef.setInput('surfaceId', 'test-surface'); - fixture.componentRef.setInput('dataContextPath', '/'); - - defaultProps = { - name: createBoundProperty('search' as const), - }; - setComponentProps(fixture, defaultProps); - }); - - it('should create', () => { - fixture.detectChanges(); - expect(component).toBeTruthy(); - }); - - it('should render named icon', () => { - fixture.detectChanges(); - const icon = fixture.nativeElement.querySelector('.a2ui-icon'); - expect(icon.textContent.trim()).toBe('search'); - }); - - it('should convert camelCase icon names to snake_case', () => { - setComponentProps(fixture, { - ...defaultProps, - name: createBoundProperty('shoppingCart' as const), - }); - fixture.detectChanges(); - const icon = fixture.nativeElement.querySelector('.a2ui-icon'); - expect(icon.textContent.trim()).toBe('shopping_cart'); - }); - - it('should map "play" to "play_arrow"', () => { - setComponentProps(fixture, { - ...defaultProps, - name: createBoundProperty('play' as const), - }); - fixture.detectChanges(); - const icon = fixture.nativeElement.querySelector('.a2ui-icon'); - expect(icon.textContent.trim()).toBe('play_arrow'); - }); - - it('should map "rewind" to "fast_rewind"', () => { - setComponentProps(fixture, { - ...defaultProps, - name: createBoundProperty('rewind' as const), - }); - fixture.detectChanges(); - const icon = fixture.nativeElement.querySelector('.a2ui-icon'); - expect(icon.textContent.trim()).toBe('fast_rewind'); - }); - - it('should map "favoriteOff" to "favorite_border"', () => { - setComponentProps(fixture, { - ...defaultProps, - name: createBoundProperty('favoriteOff' as const), - }); - fixture.detectChanges(); - const icon = fixture.nativeElement.querySelector('.a2ui-icon'); - expect(icon.textContent.trim()).toBe('favorite_border'); - }); - - it('should map "starOff" to "star_border"', () => { - setComponentProps(fixture, { - ...defaultProps, - name: createBoundProperty('starOff' as const), - }); - fixture.detectChanges(); - const icon = fixture.nativeElement.querySelector('.a2ui-icon'); - expect(icon.textContent.trim()).toBe('star_border'); - }); - - it('should render path icon', () => { - setComponentProps(fixture, { - ...defaultProps, - name: createBoundProperty({ - svgPath: 'M10 10...', - }) as unknown as ComponentToProps['name'], - }); - fixture.detectChanges(); - const svg = fixture.nativeElement.querySelector('svg'); - expect(svg).toBeTruthy(); - }); -}); diff --git a/renderers/angular/src/v0_9/catalog/basic/icon.component.ts b/renderers/angular/src/v0_9/catalog/basic/icon.component.ts deleted file mode 100644 index 67044209fc..0000000000 --- a/renderers/angular/src/v0_9/catalog/basic/icon.component.ts +++ /dev/null @@ -1,109 +0,0 @@ -/** - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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, ChangeDetectionStrategy} from '@angular/core'; -import {BasicCatalogComponent} from './basic-catalog-component'; -import {IconApi} from '@a2ui/web_core/v0_9/basic_catalog'; - -const ICON_NAME_OVERRIDES: Record = { - play: 'play_arrow', - rewind: 'fast_rewind', - favoriteOff: 'favorite_border', - starOff: 'star_border', -}; - -/** - * Angular implementation of the A2UI Icon component (v0.9). - * - * Supports both Material Icons (by name) and custom SVG icons (by path). - * - * Supported CSS variables: - * - `--a2ui-icon-size`: Controls the width, height, and font size of the icon. - * - `--a2ui-icon-font-family`: Controls the font family for icon fonts. - * - `--a2ui-icon-color`: Controls the color of the icon. - * - `--a2ui-icon-font-variation-settings`: Controls font variation settings (e.g. FILL). - */ -@Component({ - selector: 'a2ui-v09-icon', - standalone: true, - imports: [], - template: ` - @if (isSvgPath()) { - - - - } @else { - - {{ iconName() }} - - } - `, - styles: [ - ` - .a2ui-icon { - display: inline-block; - width: var(--a2ui-icon-size, 24px); - height: var(--a2ui-icon-size, 24px); - font-size: var(--a2ui-icon-size, 24px); - font-family: var(--a2ui-icon-font-family, 'Material Icons'); - color: var( - --a2ui-icon-color, - var(--a2ui-text-color-text, var(--a2ui-color-on-background, #333)) - ); - font-variation-settings: var(--a2ui-icon-font-variation-settings, 'FILL' 1); - line-height: 1; - text-transform: none; - letter-spacing: normal; - word-wrap: normal; - white-space: nowrap; - direction: ltr; - -webkit-font-smoothing: antialiased; - text-rendering: optimizeLegibility; - -moz-osx-font-smoothing: grayscale; - font-feature-settings: 'liga'; - vertical-align: middle; - } - .a2ui-icon.svg { - fill: currentColor; - } - `, - ], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class IconComponent extends BasicCatalogComponent { - readonly iconNameRaw = computed(() => this.props()['name']?.value()); - - readonly isSvgPath = computed(() => { - const name = this.iconNameRaw(); - return typeof name === 'object' && name !== null && 'svgPath' in name; - }); - - readonly svgPath = computed(() => { - const name = this.iconNameRaw(); - if (typeof name === 'object' && name !== null && 'svgPath' in name) { - return name.svgPath; - } - return ''; - }); - - readonly iconName = computed(() => { - const name = this.iconNameRaw(); - if (typeof name !== 'string') return ''; - if (ICON_NAME_OVERRIDES[name]) return ICON_NAME_OVERRIDES[name]; - // Convert camelCase to snake_case for Material Icons - return name.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`); - }); -} diff --git a/renderers/angular/src/v0_9/catalog/basic/image.component.spec.ts b/renderers/angular/src/v0_9/catalog/basic/image.component.spec.ts deleted file mode 100644 index 611f6dfc7f..0000000000 --- a/renderers/angular/src/v0_9/catalog/basic/image.component.spec.ts +++ /dev/null @@ -1,107 +0,0 @@ -/** - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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 {ComponentFixture, TestBed} from '@angular/core/testing'; -import {ImageComponent} from './image.component'; -import {A2uiRendererService} from '../../core/a2ui-renderer.service'; -import {ComponentBinder} from '../../core/component-binder.service'; -import {setComponentProps, createBoundProperty, ComponentToProps} from '@a2ui/angular/testing'; - -describe('ImageComponent', () => { - let component: ImageComponent; - let fixture: ComponentFixture; - let defaultProps: ComponentToProps; - - beforeEach(async () => { - const mockRendererService = { - surfaceGroup: { - getSurface: jasmine.createSpy('getSurface').and.returnValue({ - componentsModel: new Map(), - catalog: { - id: 'mock-catalog', - components: new Map(), - }, - }), - }, - }; - const mockBinder = jasmine.createSpyObj('ComponentBinder', ['bind']); - - await TestBed.configureTestingModule({ - imports: [ImageComponent], - providers: [ - {provide: A2uiRendererService, useValue: mockRendererService}, - {provide: ComponentBinder, useValue: mockBinder}, - ], - }).compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(ImageComponent); - component = fixture.componentInstance; - fixture.componentRef.setInput('surfaceId', 'test-surface'); - fixture.componentRef.setInput('dataContextPath', '/'); - - defaultProps = { - url: createBoundProperty('https://example.com/image.png'), - fit: createBoundProperty('cover' as const), - variant: createBoundProperty('avatar' as const), - }; - setComponentProps(fixture, defaultProps); - }); - - it('should create', () => { - fixture.detectChanges(); - expect(component).toBeTruthy(); - }); - - it('should render image with url', () => { - fixture.detectChanges(); - const img = fixture.nativeElement.querySelector('img') as HTMLImageElement; - expect(img.src).toBeTruthy(); - expect(img.style.objectFit).toBe('cover'); - expect(img.className).toContain('avatar'); - }); - - it('should render image with description', () => { - setComponentProps(fixture, { - ...defaultProps, - description: createBoundProperty('A cute cat'), - }); - fixture.detectChanges(); - const img = fixture.nativeElement.querySelector('img') as HTMLImageElement; - expect(img.alt).toBe('A cute cat'); - }); - - it('should support all specified variants', () => { - const variants = [ - 'icon', - 'avatar', - 'smallFeature', - 'mediumFeature', - 'largeFeature', - 'header', - ] as const; - for (const variant of variants) { - setComponentProps(fixture, { - ...defaultProps, - variant: createBoundProperty(variant), - }); - fixture.detectChanges(); - const img = fixture.nativeElement.querySelector('img') as HTMLImageElement; - expect(img.className).toContain(variant); - } - }); -}); diff --git a/renderers/angular/src/v0_9/catalog/basic/image.component.ts b/renderers/angular/src/v0_9/catalog/basic/image.component.ts deleted file mode 100644 index bb8b48e538..0000000000 --- a/renderers/angular/src/v0_9/catalog/basic/image.component.ts +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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, ChangeDetectionStrategy} from '@angular/core'; -import {BasicCatalogComponent} from './basic-catalog-component'; -import {ImageApi} from '@a2ui/web_core/v0_9/basic_catalog'; - -/** - * Angular implementation of the A2UI Image component (v0.9). - * - * Renders an image with configurable fit and shape variants. - * - * Supported CSS variables: - * - `--a2ui-image-border-radius`: Controls the rounded corners. - * - `--a2ui-image-icon-size`: Size of the icon variant. - * - `--a2ui-image-avatar-size`: Size of the avatar variant. - * - `--a2ui-image-small-feature-size`: Max-width of smallFeature variant. - * - `--a2ui-image-large-feature-size`: Max-height of largeFeature variant. - * - `--a2ui-image-header-size`: Height of header variant. - */ -@Component({ - selector: 'a2ui-v09-image', - standalone: true, - imports: [], - template: ` - - `, - styles: [ - ` - .a2ui-image { - display: block; - max-width: 100%; - height: auto; - border-radius: var(--a2ui-image-border-radius, var(--a2ui-border-radius, 8px)); - } - .a2ui-image.icon { - width: var(--a2ui-image-icon-size, 24px); - height: var(--a2ui-image-icon-size, 24px); - } - .a2ui-image.avatar { - width: var(--a2ui-image-avatar-size, 40px); - height: var(--a2ui-image-avatar-size, 40px); - border-radius: 50%; - } - .a2ui-image.smallFeature { - max-width: var(--a2ui-image-small-feature-size, 100px); - } - .a2ui-image.largeFeature { - max-height: var(--a2ui-image-large-feature-size, 400px); - } - .a2ui-image.header { - height: var(--a2ui-image-header-size, 200px); - } - `, - ], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class ImageComponent extends BasicCatalogComponent { - readonly url = computed(() => this.props()['url']?.value()); - readonly description = computed(() => this.props()['description']?.value() || ''); - readonly fit = computed(() => this.props()['fit']?.value() || 'cover'); - readonly variant = computed(() => this.props()['variant']?.value() || 'default'); -} diff --git a/renderers/angular/src/v0_9/catalog/basic/list.component.spec.ts b/renderers/angular/src/v0_9/catalog/basic/list.component.spec.ts deleted file mode 100644 index 7bb7dfc476..0000000000 --- a/renderers/angular/src/v0_9/catalog/basic/list.component.spec.ts +++ /dev/null @@ -1,140 +0,0 @@ -/** - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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 {ComponentFixture, TestBed} from '@angular/core/testing'; -import {Component, input} from '@angular/core'; -import {ListComponent} from './list.component'; -import {ComponentModel} from '@a2ui/web_core/v0_9'; -import {A2uiRendererService} from '../../core/a2ui-renderer.service'; -import {ComponentBinder, Child} from '../../core/component-binder.service'; -import {setComponentProps, createBoundProperty, ComponentToProps} from '@a2ui/angular/testing'; - -@Component({ - selector: 'dummy-text-for-list', - template: '
{{text}}
', - standalone: true, -}) -class DummyTextComponent { - text?: string; - props = input>(); - surfaceId = input(); - componentId = input(); - dataContextPath = input(); -} - -describe('ListComponent', () => { - let component: ListComponent; - let fixture: ComponentFixture; - let defaultProps: ComponentToProps; - - beforeEach(async () => { - const mockRendererService = { - surfaceGroup: { - getSurface: jasmine.createSpy('getSurface').and.returnValue({ - componentsModel: new Map([ - ['child-1', new ComponentModel('child-1', 'Text', {text: {value: 'Child 1'}})], - ['child-2', new ComponentModel('child-2', 'Text', {text: {value: 'Child 2'}})], - ]), - catalog: { - id: 'mock-catalog', - components: new Map([['Text', {type: 'Text', component: DummyTextComponent}]]), - }, - }), - }, - }; - const mockBinder = jasmine.createSpyObj('ComponentBinder', ['bind']); - - await TestBed.configureTestingModule({ - imports: [ListComponent], - providers: [ - {provide: A2uiRendererService, useValue: mockRendererService}, - {provide: ComponentBinder, useValue: mockBinder}, - ], - }).compileComponents(); - - fixture = TestBed.createComponent(ListComponent); - component = fixture.componentInstance; - fixture.componentRef.setInput('surfaceId', 'test-surface'); - fixture.componentRef.setInput('dataContextPath', '/'); - - defaultProps = { - children: createBoundProperty([]), - direction: createBoundProperty<'vertical' | 'horizontal' | undefined>('vertical'), - listStyle: createBoundProperty<'none' | 'ordered' | 'unordered' | undefined>('none'), - }; - setComponentProps(fixture, defaultProps); - }); - - it('should create', () => { - fixture.detectChanges(); - expect(component).toBeTruthy(); - }); - - it('should render children', () => { - setComponentProps(fixture, { - ...defaultProps, - children: createBoundProperty([ - {id: 'child-1', basePath: '/'}, - {id: 'child-2', basePath: '/'}, - ]), - }); - fixture.detectChanges(); - const hosts = fixture.nativeElement.querySelectorAll('a2ui-v09-component-host'); - expect(hosts.length).toBe(2); - }); - - it('should render as ordered list', () => { - setComponentProps(fixture, { - ...defaultProps, - children: createBoundProperty([{id: 'child-1', basePath: '/'}]), - listStyle: createBoundProperty<'none' | 'ordered' | 'unordered' | undefined>('ordered'), - }); - fixture.detectChanges(); - expect(fixture.nativeElement.querySelector('ol')).toBeTruthy(); - }); - - it('should render as unordered list', () => { - setComponentProps(fixture, { - ...defaultProps, - children: createBoundProperty([{id: 'child-1', basePath: '/'}]), - listStyle: createBoundProperty<'none' | 'ordered' | 'unordered' | undefined>('unordered'), - }); - fixture.detectChanges(); - expect(fixture.nativeElement.querySelector('ul')).toBeTruthy(); - }); - - it('should render fallback list when style is not list style', () => { - setComponentProps(fixture, { - ...defaultProps, - children: createBoundProperty([{id: 'child-1', basePath: '/'}]), - listStyle: createBoundProperty('div' as 'none' | 'ordered' | 'unordered' | undefined), - }); - fixture.detectChanges(); - const divList = fixture.nativeElement.querySelector('.a2ui-list'); - expect(divList.tagName.toLowerCase()).toBe('div'); - }); - - it('should apply horizontal orientation class', () => { - setComponentProps(fixture, { - ...defaultProps, - children: createBoundProperty([{id: 'child-1', basePath: '/'}]), - direction: createBoundProperty<'vertical' | 'horizontal' | undefined>('horizontal'), - }); - fixture.detectChanges(); - const list = fixture.nativeElement.querySelector('.a2ui-list'); - expect(list.classList).toContain('horizontal'); - }); -}); diff --git a/renderers/angular/src/v0_9/catalog/basic/list.component.ts b/renderers/angular/src/v0_9/catalog/basic/list.component.ts deleted file mode 100644 index e5400bbd62..0000000000 --- a/renderers/angular/src/v0_9/catalog/basic/list.component.ts +++ /dev/null @@ -1,120 +0,0 @@ -/** - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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, ChangeDetectionStrategy} from '@angular/core'; -import {ComponentHostComponent} from '../../core/component-host.component'; -import {BasicCatalogComponent} from './basic-catalog-component'; -import {Child} from '../../core/component-binder.service'; -import {ListApi} from '@a2ui/web_core/v0_9/basic_catalog'; - -/** - * Angular implementation of the A2UI List component (v0.9). - * - * Renders a list of child components with support for ordered, unordered, - * and unstyled layouts in both vertical and horizontal directions. - * - * Supported CSS variables: - * - `--a2ui-list-gap`: Controls the gap between items. - * - `--a2ui-list-padding`: Controls the padding (applied to padding-inline-start). - */ -@Component({ - selector: 'a2ui-v09-list', - standalone: true, - imports: [ComponentHostComponent], - template: ` - @switch (listTag()) { - @case ('ol') { -
    - @for (child of children(); track trackBy($index, child)) { -
  1. - - -
  2. - } -
- } - @case ('ul') { -
    - @for (child of children(); track trackBy($index, child)) { -
  • - - -
  • - } -
- } - @default { -
- @for (child of children(); track trackBy($index, child)) { -
- - -
- } -
- } - } - `, - styles: [ - ` - .a2ui-list { - display: flex; - padding-inline-start: var(--a2ui-list-padding, var(--a2ui-spacing-l, 24px)); - margin: 0; - } - .a2ui-list.vertical { - flex-direction: column; - gap: var(--a2ui-list-gap, var(--a2ui-spacing-s, 8px)); - } - .a2ui-list.horizontal { - flex-direction: row; - gap: var(--a2ui-list-gap, var(--a2ui-spacing-m, 16px)); - list-style-position: inside; - } - .a2ui-list-item-none { - display: block; - } - .horizontal .a2ui-list-item-none { - display: inline-block; - } - `, - ], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class ListComponent extends BasicCatalogComponent { - readonly listStyle = computed(() => this.props()['listStyle']?.value()); - readonly direction = computed(() => this.props()['direction']?.value() || 'vertical'); - readonly children = computed(() => this.props()['children'].value()); - - readonly listTag = computed(() => { - const style = this.listStyle(); - if (style === 'ordered') return 'ol'; - if (style === 'unordered') return 'ul'; - return 'div'; - }); - - readonly styleType = computed(() => { - const style = this.listStyle(); - if (style === 'none') return 'none'; - return ''; - }); - - /** - * Track-by function to ensure stable change detection for list items. - * Uses the full resolved path (`basePath/id`) to uniquely identify items. - */ - readonly trackBy = (index: number, item: Child) => `${item.basePath}/${item.id}`; -} diff --git a/renderers/angular/src/v0_9/catalog/basic/modal.component.spec.ts b/renderers/angular/src/v0_9/catalog/basic/modal.component.spec.ts deleted file mode 100644 index ce8e545134..0000000000 --- a/renderers/angular/src/v0_9/catalog/basic/modal.component.spec.ts +++ /dev/null @@ -1,157 +0,0 @@ -/** - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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 {ComponentFixture, TestBed} from '@angular/core/testing'; -import {Component, input} from '@angular/core'; -import {By} from '@angular/platform-browser'; -import {ModalComponent} from './modal.component'; -import {ComponentModel} from '@a2ui/web_core/v0_9'; -import {A2uiRendererService} from '../../core/a2ui-renderer.service'; -import {ComponentBinder} from '../../core/component-binder.service'; -import {setComponentProps, createBoundProperty, ComponentToProps} from '@a2ui/angular/testing'; - -@Component({ - selector: 'dummy-text-for-modal', - template: '
{{text}}
', - standalone: true, -}) -class DummyTextComponent { - text?: string; - props = input>(); - surfaceId = input(); - componentId = input(); - dataContextPath = input(); -} - -describe('ModalComponent', () => { - let component: ModalComponent; - let fixture: ComponentFixture; - let defaultProps: ComponentToProps; - - beforeEach(async () => { - const mockRendererService = { - surfaceGroup: { - getSurface: jasmine.createSpy('getSurface').and.returnValue({ - componentsModel: new Map([ - ['trigger-btn', new ComponentModel('trigger-btn', 'Text', {text: {value: 'Open'}})], - [ - 'modal-content', - new ComponentModel('modal-content', 'Text', {text: {value: 'Modal'}}), - ], - ]), - catalog: { - id: 'mock-catalog', - components: new Map([['Text', {type: 'Text', component: DummyTextComponent}]]), - }, - }), - }, - }; - const mockBinder = jasmine.createSpyObj('ComponentBinder', ['bind']); - - await TestBed.configureTestingModule({ - imports: [ModalComponent], - providers: [ - {provide: A2uiRendererService, useValue: mockRendererService}, - {provide: ComponentBinder, useValue: mockBinder}, - ], - }).compileComponents(); - - fixture = TestBed.createComponent(ModalComponent); - component = fixture.componentInstance; - fixture.componentRef.setInput('surfaceId', 'test-surface'); - fixture.componentRef.setInput('dataContextPath', '/'); - - defaultProps = {} as ComponentToProps; - setComponentProps(fixture, defaultProps); - }); - - it('should create', () => { - fixture.detectChanges(); - expect(component).toBeTruthy(); - }); - - it('should render trigger and open modal on click', () => { - setComponentProps(fixture, { - ...defaultProps, - trigger: createBoundProperty({id: 'trigger-btn', basePath: '/'}), - content: createBoundProperty({id: 'modal-content', basePath: '/'}), - }); - fixture.detectChanges(); - const triggerHost = fixture.debugElement.query( - By.css('.a2ui-modal-trigger a2ui-v09-component-host'), - ); - expect(triggerHost.componentInstance.componentKey()).toEqual({ - id: 'trigger-btn', - basePath: '/', - }); - - expect(fixture.nativeElement.querySelector('.a2ui-modal-overlay')).toBeFalsy(); - - fixture.nativeElement.querySelector('.a2ui-modal-trigger').click(); - fixture.detectChanges(); - - const overlay = fixture.nativeElement.querySelector('.a2ui-modal-overlay'); - expect(overlay).toBeTruthy(); - const contentHost = fixture.debugElement.query( - By.css('.a2ui-modal-overlay a2ui-v09-component-host'), - ); - expect(contentHost.componentInstance.componentKey()).toEqual({ - id: 'modal-content', - basePath: '/', - }); - }); - - it('should close modal when close button clicked', () => { - setComponentProps(fixture, { - ...defaultProps, - trigger: createBoundProperty({id: 'trigger-btn', basePath: '/'}), - content: createBoundProperty({id: 'modal-content', basePath: '/'}), - }); - fixture.detectChanges(); - - fixture.nativeElement.querySelector('.a2ui-modal-trigger').click(); - fixture.detectChanges(); - expect(fixture.nativeElement.querySelector('.a2ui-modal-overlay')).toBeTruthy(); - - fixture.nativeElement.querySelector('.a2ui-modal-close').click(); - fixture.detectChanges(); - expect(fixture.nativeElement.querySelector('.a2ui-modal-overlay')).toBeFalsy(); - }); - - it('should close modal when overlay clicked', () => { - setComponentProps(fixture, { - ...defaultProps, - trigger: createBoundProperty({id: 'trigger-btn', basePath: '/'}), - content: createBoundProperty({id: 'modal-content', basePath: '/'}), - }); - fixture.detectChanges(); - - fixture.nativeElement.querySelector('.a2ui-modal-trigger').click(); - fixture.detectChanges(); - expect(fixture.nativeElement.querySelector('.a2ui-modal-overlay')).toBeTruthy(); - - fixture.nativeElement.querySelector('.a2ui-modal-overlay').click(); - fixture.detectChanges(); - expect(fixture.nativeElement.querySelector('.a2ui-modal-overlay')).toBeFalsy(); - }); - - it('should handle missing trigger or content', () => { - fixture.detectChanges(); - expect(component.trigger()).toBeUndefined(); - expect(component.content()).toBeUndefined(); - expect(fixture.nativeElement.querySelector('a2ui-v09-component-host')).toBeFalsy(); - }); -}); diff --git a/renderers/angular/src/v0_9/catalog/basic/modal.component.ts b/renderers/angular/src/v0_9/catalog/basic/modal.component.ts deleted file mode 100644 index 1e74751fe6..0000000000 --- a/renderers/angular/src/v0_9/catalog/basic/modal.component.ts +++ /dev/null @@ -1,133 +0,0 @@ -/** - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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, ChangeDetectionStrategy, signal} from '@angular/core'; -import {ComponentHostComponent} from '../../core/component-host.component'; -import {BasicCatalogComponent} from './basic-catalog-component'; -import {ModalApi} from '@a2ui/web_core/v0_9/basic_catalog'; - -/** - * Angular implementation of the A2UI Modal component (v0.9). - * - * Renders a trigger component that opening an overlay containing a content component. - * - * Supported CSS variables: - * - `--a2ui-modal-background`: Controls the background of the modal content. - * - `--a2ui-modal-padding`: Controls the padding of the modal content. - * - `--a2ui-modal-border-radius`: Controls the border radius of the modal content. - * - `--a2ui-modal-box-shadow`: Controls the box shadow of the modal content. - * - `--a2ui-modal-backdrop-bg`: Controls the background of the backdrop. - */ -@Component({ - selector: 'a2ui-v09-modal', - standalone: true, - imports: [ComponentHostComponent], - template: ` -
-
- @if (trigger()) { - - - } -
- - @if (isOpen()) { -
- -
- } -
- `, - styles: [ - ` - .a2ui-modal-wrapper { - display: inline-block; - } - .a2ui-modal-trigger { - cursor: pointer; - } - .a2ui-modal-overlay { - position: fixed; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - background: var(--a2ui-modal-backdrop-bg, rgba(0, 0, 0, 0.5)); - display: flex; - justify-content: center; - align-items: center; - z-index: 1000; - } - .a2ui-modal-content { - background: var(--a2ui-modal-background, var(--a2ui-color-surface, white)); - padding: var(--a2ui-modal-padding, var(--a2ui-spacing-xl, 32px)); - border-radius: var(--a2ui-modal-border-radius, var(--a2ui-border-radius, 8px)); - position: relative; - min-width: 300px; - max-width: 80%; - max-height: 80%; - overflow-y: auto; - box-shadow: var(--a2ui-modal-box-shadow, 0 10px 25px rgba(0, 0, 0, 0.2)); - } - .a2ui-modal-close { - position: absolute; - top: 10px; - right: 15px; - border: none; - background: none; - font-size: 24px; - cursor: pointer; - color: var(--a2ui-text-caption-color, #999); - } - .a2ui-modal-close:hover { - color: var(--a2ui-text-color, #333); - } - `, - ], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class ModalComponent extends BasicCatalogComponent { - isOpen = signal(false); - - readonly trigger = computed(() => this.props()['trigger']?.value()); - readonly content = computed(() => this.props()['content']?.value()); - - openModal() { - this.isOpen.set(true); - } - - closeModal() { - this.isOpen.set(false); - } -} diff --git a/renderers/angular/src/v0_9/catalog/basic/row.component.spec.ts b/renderers/angular/src/v0_9/catalog/basic/row.component.spec.ts deleted file mode 100644 index 408276819e..0000000000 --- a/renderers/angular/src/v0_9/catalog/basic/row.component.spec.ts +++ /dev/null @@ -1,156 +0,0 @@ -/** - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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 {ComponentFixture, TestBed} from '@angular/core/testing'; -import {Component, input, signal} from '@angular/core'; -import {RowComponent} from './row.component'; -import {ComponentModel} from '@a2ui/web_core/v0_9'; -import {A2uiRendererService} from '../../core/a2ui-renderer.service'; -import {ComponentBinder} from '../../core/component-binder.service'; -import {By} from '@angular/platform-browser'; -import {setComponentProps, createBoundProperty, ComponentToProps} from '@a2ui/angular/testing'; - -@Component({ - standalone: true, - selector: 'dummy-child', - template: 'Dummy Child', -}) -class DummyChild { - props = input(); - surfaceId = input(); - componentId = input(); - dataContextPath = input(); -} - -describe('RowComponent', () => { - let component: RowComponent; - let fixture: ComponentFixture; - let mockRendererService: any; - let mockSurface: any; - let mockSurfaceGroup: any; - let mockBinder: any; - let defaultProps: ComponentToProps; - - beforeEach(async () => { - mockSurface = { - componentsModel: new Map([ - ['child1', new ComponentModel('child1', 'Child', {})], - ['child2', new ComponentModel('child2', 'Child', {})], - ['template1', new ComponentModel('template1', 'Child', {})], - ]), - catalog: { - id: 'test-catalog', - components: new Map([['Child', {component: DummyChild}]]), - }, - }; - - mockSurfaceGroup = { - getSurface: jasmine.createSpy('getSurface').and.returnValue(mockSurface), - }; - - mockRendererService = { - surfaceGroup: mockSurfaceGroup, - }; - - mockBinder = jasmine.createSpyObj('ComponentBinder', ['bind']); - mockBinder.bind.and.returnValue({text: {value: () => 'bound'}}); - - await TestBed.configureTestingModule({ - imports: [RowComponent], - providers: [ - {provide: A2uiRendererService, useValue: mockRendererService}, - {provide: ComponentBinder, useValue: mockBinder}, - ], - }).compileComponents(); - - fixture = TestBed.createComponent(RowComponent); - component = fixture.componentInstance; - fixture.componentRef.setInput('surfaceId', 'surf1'); - - defaultProps = { - justify: createBoundProperty('center' as const), - align: createBoundProperty('stretch' as const), - children: createBoundProperty([ - {id: 'child1', basePath: '/'}, - {id: 'child2', basePath: '/'}, - ]), - }; - setComponentProps(fixture, defaultProps); - }); - - it('should create', () => { - fixture.detectChanges(); - expect(component).toBeTruthy(); - }); - - it('should apply flex styles from props', () => { - fixture.detectChanges(); - const style = window.getComputedStyle(fixture.debugElement.nativeElement); - expect(style.justifyContent).toBe('center'); - expect(style.alignItems).toBe('stretch'); - }); - - it('should render non-repeating children', () => { - fixture.detectChanges(); - const hosts = fixture.debugElement.queryAll(By.css('a2ui-v09-component-host')); - expect(hosts.length).toBe(2); - expect(hosts[0].componentInstance.componentKey()).toEqual({id: 'child1', basePath: '/'}); - expect(hosts[1].componentInstance.componentKey()).toEqual({id: 'child2', basePath: '/'}); - }); - - it('should render repeating children', () => { - setComponentProps(fixture, { - ...defaultProps, - children: { - value: signal([ - {id: 'template1', basePath: '/items/0'}, - {id: 'template1', basePath: '/items/1'}, - ]), - raw: { - componentId: 'template1', - path: 'items', - }, - template: { - id: 'template1', - path: 'items', - }, - onUpdate: jasmine.createSpy('onUpdate'), - }, - }); - fixture.detectChanges(); - - const hosts = fixture.debugElement.queryAll(By.css('a2ui-v09-component-host')); - expect(hosts.length).toBe(2); - expect(hosts[0].componentInstance.componentKey()).toEqual({ - id: 'template1', - basePath: '/items/0', - }); - expect(hosts[1].componentInstance.componentKey()).toEqual({ - id: 'template1', - basePath: '/items/1', - }); - }); - - it('should handle missing justify and align properties', () => { - setComponentProps(fixture, { - children: createBoundProperty([{id: 'child1', basePath: '/'}]), - }); - fixture.detectChanges(); - const div = fixture.debugElement; - expect(div.styles['justify-content']).toBeFalsy(); - expect(div.styles['align-items']).toBeFalsy(); - }); -}); diff --git a/renderers/angular/src/v0_9/catalog/basic/row.component.ts b/renderers/angular/src/v0_9/catalog/basic/row.component.ts deleted file mode 100644 index de5d32256d..0000000000 --- a/renderers/angular/src/v0_9/catalog/basic/row.component.ts +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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, ChangeDetectionStrategy} from '@angular/core'; -import {ComponentHostComponent} from '../../core/component-host.component'; -import {Child} from '../../core/component-binder.service'; -import {BasicCatalogComponent} from './basic-catalog-component'; -import {JUSTIFY_MAP, ALIGN_MAP} from './utils'; -import {RowApi} from '@a2ui/web_core/v0_9/basic_catalog'; - -/** - * Angular implementation of the A2UI Row component (v0.9). - * - * Arranges child components in a horizontal flex layout. Supports both static - * lists of children and repeating templates bound to a data collection. - * - * Supported CSS variables: - * - `--a2ui-row-gap`: Controls the gap between items in the row. Defaults to `--a2ui-spacing-m` (16px). - */ -@Component({ - selector: 'a2ui-v09-row', - standalone: true, - imports: [ComponentHostComponent], - host: { - '[style.display]': '"flex"', - '[style.flex-direction]': '"row"', - '[style.gap]': '"var(--a2ui-row-gap, var(--a2ui-spacing-m, 16px))"', - '[style.justify-content]': 'justify()', - '[style.align-items]': 'align()', - }, - template: ` - @for (child of children(); track trackChild($index, child)) { - - - } - `, - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class RowComponent extends BasicCatalogComponent { - protected readonly justify = computed(() => { - const val = this.props()['justify']?.value(); - return val ? JUSTIFY_MAP[val] || val : undefined; - }); - protected readonly align = computed(() => { - const val = this.props()['align']?.value(); - return val ? ALIGN_MAP[val] || val : undefined; - }); - - protected readonly children = computed(() => this.props()['children'].value() || []); - - protected trackChild(_index: number, child: Child) { - return `${child.basePath}/${child.id}`; - } -} diff --git a/renderers/angular/src/v0_9/catalog/basic/slider.component.spec.ts b/renderers/angular/src/v0_9/catalog/basic/slider.component.spec.ts deleted file mode 100644 index d56857f083..0000000000 --- a/renderers/angular/src/v0_9/catalog/basic/slider.component.spec.ts +++ /dev/null @@ -1,96 +0,0 @@ -/** - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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 {ComponentFixture, TestBed} from '@angular/core/testing'; -import {signal as angularSignal} from '@angular/core'; -import {SliderComponent} from './slider.component'; -import {A2uiRendererService} from '../../core/a2ui-renderer.service'; -import {ComponentBinder} from '../../core/component-binder.service'; -import {setComponentProps, createBoundProperty, ComponentToProps} from '@a2ui/angular/testing'; - -describe('SliderComponent', () => { - let component: SliderComponent; - let fixture: ComponentFixture; - let defaultProps: ComponentToProps; - - beforeEach(async () => { - const mockRendererService = { - surfaceGroup: { - getSurface: jasmine.createSpy('getSurface').and.returnValue({ - componentsModel: new Map(), - catalog: { - id: 'mock-catalog', - components: new Map(), - }, - }), - }, - }; - const mockBinder = jasmine.createSpyObj('ComponentBinder', ['bind']); - - await TestBed.configureTestingModule({ - imports: [SliderComponent], - providers: [ - {provide: A2uiRendererService, useValue: mockRendererService}, - {provide: ComponentBinder, useValue: mockBinder}, - ], - }).compileComponents(); - - fixture = TestBed.createComponent(SliderComponent); - component = fixture.componentInstance; - fixture.componentRef.setInput('surfaceId', 'test-surface'); - fixture.componentRef.setInput('dataContextPath', '/'); - - defaultProps = { - label: createBoundProperty(''), - min: createBoundProperty(0), - max: createBoundProperty(100), - value: createBoundProperty(0), - isValid: createBoundProperty(true), - validationErrors: createBoundProperty([]), - }; - setComponentProps(fixture, defaultProps); - }); - - it('should create', () => { - fixture.detectChanges(); - expect(component).toBeTruthy(); - }); - - it('should render slider with value', () => { - setComponentProps(fixture, { - ...defaultProps, - label: createBoundProperty('Brightness'), - value: createBoundProperty(50), - }); - fixture.detectChanges(); - const input = fixture.nativeElement.querySelector('input'); - expect(input.value).toBe('50'); - expect(fixture.nativeElement.textContent).toContain('Brightness'); - }); - - it('should call onUpdate when slider value changes', () => { - const onUpdateSpy = jasmine.createSpy('onUpdate'); - setComponentProps(fixture, { - ...defaultProps, - value: {value: angularSignal(50), raw: 50, onUpdate: onUpdateSpy}, - }); - fixture.detectChanges(); - const input = fixture.nativeElement.querySelector('input'); - input.value = '75'; - input.dispatchEvent(new Event('input')); - expect(onUpdateSpy).toHaveBeenCalledWith(75); - }); -}); diff --git a/renderers/angular/src/v0_9/catalog/basic/slider.component.ts b/renderers/angular/src/v0_9/catalog/basic/slider.component.ts deleted file mode 100644 index 9622e2388e..0000000000 --- a/renderers/angular/src/v0_9/catalog/basic/slider.component.ts +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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, ChangeDetectionStrategy} from '@angular/core'; -import {BasicCatalogComponent} from './basic-catalog-component'; -import {SliderApi} from '@a2ui/web_core/v0_9/basic_catalog'; - -/** - * Angular implementation of the A2UI Slider component (v0.9). - * - * Renders a range input slider with a label and its current value. - * - * Supported CSS variables: - * - `--a2ui-slider-margin`: Controls the margin of the container. - * - `--a2ui-slider-label-font-size`: Controls the font size of the label. - * - `--a2ui-slider-label-font-weight`: Controls the font weight of the label. - * - `--a2ui-slider-thumb-color`: Controls the accent color of the thumb. - * - `--a2ui-slider-track-color`: Controls the background of the track. - */ -@Component({ - selector: 'a2ui-v09-slider', - standalone: true, - imports: [], - template: ` -
-
- {{ label() }} - {{ value() }} -
- -
- `, - styles: [ - ` - .a2ui-slider-container { - width: 100%; - display: flex; - flex-direction: column; - gap: var(--a2ui-spacing-xs, 4px); - margin: var(--a2ui-slider-margin, var(--a2ui-spacing-m, 16px)); - } - .a2ui-slider-header { - display: flex; - justify-content: space-between; - font-size: var( - --a2ui-slider-label-font-size, - var(--a2ui-label-font-size, var(--a2ui-font-size-s, 14px)) - ); - font-weight: var(--a2ui-slider-label-font-weight, bold); - color: var(--a2ui-text-color-text, var(--a2ui-color-on-background, #333)); - } - .a2ui-slider { - width: 100%; - cursor: pointer; - accent-color: var(--a2ui-slider-thumb-color, var(--a2ui-color-primary, #007bff)); - background: var(--a2ui-slider-track-color, var(--a2ui-color-secondary, #e9ecef)); - } - `, - ], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class SliderComponent extends BasicCatalogComponent { - readonly label = computed(() => this.props()['label']?.value()); - readonly value = computed(() => this.props()['value']?.value()); - readonly min = computed(() => this.props()['min']?.value() ?? 0); - readonly max = computed(() => this.props()['max']?.value() ?? 100); - - handleInput(event: Event) { - const val = Number((event.target as HTMLInputElement).value); - this.props()['value']?.onUpdate(val); - } -} diff --git a/renderers/angular/src/v0_9/catalog/basic/tabs.component.spec.ts b/renderers/angular/src/v0_9/catalog/basic/tabs.component.spec.ts deleted file mode 100644 index 1dc1980d2a..0000000000 --- a/renderers/angular/src/v0_9/catalog/basic/tabs.component.spec.ts +++ /dev/null @@ -1,122 +0,0 @@ -/** - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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 {ComponentFixture, TestBed} from '@angular/core/testing'; -import {Component, input} from '@angular/core'; -import {By} from '@angular/platform-browser'; -import {TabsComponent} from './tabs.component'; -import {ComponentModel, DynamicString} from '@a2ui/web_core/v0_9'; -import {A2uiRendererService} from '../../core/a2ui-renderer.service'; -import {ComponentBinder} from '../../core/component-binder.service'; -import {setComponentProps, createBoundProperty, ComponentToProps} from '@a2ui/angular/testing'; - -@Component({ - selector: 'dummy-text-for-tabs', - template: '
{{text}}
', - standalone: true, -}) -class DummyTextComponent { - text?: string; - props = input>(); - surfaceId = input(); - componentId = input(); - dataContextPath = input(); -} - -describe('TabsComponent', () => { - let component: TabsComponent; - let fixture: ComponentFixture; - let defaultProps: ComponentToProps; - - beforeEach(async () => { - const mockRendererService = { - surfaceGroup: { - getSurface: jasmine.createSpy('getSurface').and.returnValue({ - componentsModel: new Map([ - ['content-1', new ComponentModel('content-1', 'Text', {text: {value: 'Content 1'}})], - ['content-2', new ComponentModel('content-2', 'Text', {text: {value: 'Content 2'}})], - ]), - catalog: { - id: 'mock-catalog', - components: new Map([['Text', {type: 'Text', component: DummyTextComponent}]]), - }, - }), - }, - }; - const mockBinder = jasmine.createSpyObj('ComponentBinder', ['bind']); - - await TestBed.configureTestingModule({ - imports: [TabsComponent], - providers: [ - {provide: A2uiRendererService, useValue: mockRendererService}, - {provide: ComponentBinder, useValue: mockBinder}, - ], - }).compileComponents(); - - fixture = TestBed.createComponent(TabsComponent); - component = fixture.componentInstance; - fixture.componentRef.setInput('surfaceId', 'test-surface'); - fixture.componentRef.setInput('dataContextPath', '/'); - - defaultProps = { - tabs: createBoundProperty<{title: DynamicString; child: string}[]>([]), - }; - setComponentProps(fixture, defaultProps); - }); - - it('should create', () => { - fixture.detectChanges(); - expect(component).toBeTruthy(); - }); - - it('should render tabs and switch content', () => { - setComponentProps(fixture, { - ...defaultProps, - tabs: createBoundProperty<{title: DynamicString; child: string}[]>([ - {title: 'Tab 1', child: 'content-1'}, - {title: 'Tab 2', child: 'content-2'}, - ]), - }); - fixture.detectChanges(); - const tabs = fixture.nativeElement.querySelectorAll('.a2ui-tab-button'); - expect(tabs.length).toBe(2); - expect(tabs[0].textContent).toContain('Tab 1'); - - let host = fixture.debugElement.query(By.css('a2ui-v09-component-host')); - expect(host.componentInstance.componentKey()).toEqual({id: 'content-1', basePath: '/'}); - - tabs[1].click(); - fixture.detectChanges(); - host = fixture.debugElement.query(By.css('a2ui-v09-component-host')); - expect(host.componentInstance.componentKey()).toEqual({id: 'content-2', basePath: '/'}); - }); - - it('should handle missing tabs property', () => { - fixture.detectChanges(); - expect(component.tabs()).toEqual([]); - expect(fixture.nativeElement.querySelectorAll('.a2ui-tab-button').length).toBe(0); - }); - - it('should handle empty tabs array', () => { - setComponentProps(fixture, { - ...defaultProps, - tabs: createBoundProperty<{title: DynamicString; child: string}[]>([]), - }); - fixture.detectChanges(); - expect(component.tabs()).toEqual([]); - expect(fixture.nativeElement.querySelectorAll('.a2ui-tab-button').length).toBe(0); - }); -}); diff --git a/renderers/angular/src/v0_9/catalog/basic/tabs.component.ts b/renderers/angular/src/v0_9/catalog/basic/tabs.component.ts deleted file mode 100644 index de96e3656a..0000000000 --- a/renderers/angular/src/v0_9/catalog/basic/tabs.component.ts +++ /dev/null @@ -1,116 +0,0 @@ -/** - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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, ChangeDetectionStrategy, signal} from '@angular/core'; -import {ComponentHostComponent} from '../../core/component-host.component'; -import {BasicCatalogComponent} from './basic-catalog-component'; -import {TabsApi} from '@a2ui/web_core/v0_9/basic_catalog'; - -/** - * Angular implementation of the A2UI Tabs component (v0.9). - * - * Renders a set of tabs where each tab has a label and associated content. - * Manages the active tab state internally. - * - * Supported CSS variables: - * - `--a2ui-tabs-border`: Controls the border of the tab bar. - * - `--a2ui-tabs-header-background`: Controls the background of tab buttons. - * - `--a2ui-tabs-header-color`: Controls the text color of tab buttons. - * - `--a2ui-tabs-header-background-active`: Controls the background of the active tab button. - * - `--a2ui-tabs-header-color-active`: Controls the text color of the active tab button. - * - `--a2ui-tabs-content-padding`: Controls the padding of the tab content. - */ -@Component({ - selector: 'a2ui-v09-tabs', - standalone: true, - imports: [ComponentHostComponent], - template: ` -
-
- @for (tab of tabs(); track tab; let i = $index) { - - } -
- @if (normalizedActiveTabChild()) { -
- - -
- } -
- `, - styles: [ - ` - .a2ui-tabs { - display: flex; - flex-direction: column; - width: 100%; - } - .a2ui-tab-bar { - display: flex; - border-bottom: var(--a2ui-tabs-border, 2px solid var(--a2ui-color-border, #eee)); - gap: var(--a2ui-spacing-m, 16px); - } - .a2ui-tab-button { - padding: var(--a2ui-spacing-s, 8px) var(--a2ui-spacing-m, 16px); - border: none; - background: var(--a2ui-tabs-header-background, transparent); - cursor: pointer; - font-weight: 500; - color: var(--a2ui-tabs-header-color, var(--a2ui-text-caption-color, #666)); - border-bottom: 2px solid transparent; - margin-bottom: -2px; - } - .a2ui-tab-button.active { - background: var(--a2ui-tabs-header-background-active, transparent); - color: var(--a2ui-tabs-header-color-active, var(--a2ui-color-primary, #007bff)); - border-bottom: 2px solid var(--a2ui-color-primary, #007bff); - } - .a2ui-tab-content { - padding: var(--a2ui-tabs-content-padding, var(--a2ui-spacing-m, 16px) 0); - } - `, - ], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class TabsComponent extends BasicCatalogComponent { - activeTabIndex = signal(0); - - readonly tabs = computed(() => this.props()['tabs']?.value() || []); - readonly activeTab = computed(() => this.tabs()[this.activeTabIndex()]); - - protected readonly normalizedActiveTabChild = computed(() => { - const child = this.activeTab()?.child; - if (!child) return null; - if (typeof child === 'object' && child !== null && 'id' in child) { - return child as {id: string; basePath: string}; - } - return {id: child as string, basePath: this.dataContextPath()}; - }); - - setActiveTab(index: number) { - this.activeTabIndex.set(index); - } -} diff --git a/renderers/angular/src/v0_9/catalog/basic/text-field.component.spec.ts b/renderers/angular/src/v0_9/catalog/basic/text-field.component.spec.ts deleted file mode 100644 index 77632d76ac..0000000000 --- a/renderers/angular/src/v0_9/catalog/basic/text-field.component.spec.ts +++ /dev/null @@ -1,136 +0,0 @@ -/** - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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 {ComponentFixture, TestBed} from '@angular/core/testing'; -import {TextFieldComponent} from './text-field.component'; -import {A2uiRendererService, A2UI_RENDERER_CONFIG} from '../../core/a2ui-renderer.service'; -import {setComponentProps, createBoundProperty, ComponentToProps} from '@a2ui/angular/testing'; -import {By} from '@angular/platform-browser'; - -describe('TextFieldComponent', () => { - let component: TextFieldComponent; - let fixture: ComponentFixture; - let defaultProps: ComponentToProps; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [TextFieldComponent], - providers: [A2uiRendererService, {provide: A2UI_RENDERER_CONFIG, useValue: {catalogs: []}}], - }).compileComponents(); - - fixture = TestBed.createComponent(TextFieldComponent); - component = fixture.componentInstance; - fixture.componentRef.setInput('surfaceId', 'surf1'); - defaultProps = { - label: createBoundProperty('Username'), - value: createBoundProperty('testuser'), - variant: createBoundProperty('shortText' as const), - isValid: createBoundProperty(true), - validationErrors: createBoundProperty([]), - }; - setComponentProps(fixture, defaultProps); - }); - - it('should create', () => { - fixture.detectChanges(); - expect(component).toBeTruthy(); - }); - - it('should render label if provided', () => { - fixture.detectChanges(); - const label = fixture.debugElement.query(By.css('label')); - expect(label.nativeElement.textContent).toBe('Username'); - }); - - it('should not render label if not provided', () => { - setComponentProps(fixture, { - ...defaultProps, - label: createBoundProperty(null as any), - }); - fixture.detectChanges(); - const label = fixture.debugElement.query(By.css('label')); - expect(label).toBeFalsy(); - }); - it('should render input with correct value', () => { - fixture.detectChanges(); - const input = fixture.debugElement.query(By.css('input')); - expect(input.nativeElement.value).toBe('testuser'); - }); - - it('should return correct input type based on variant', () => { - expect(component.inputType()).toBe('text'); - - setComponentProps(fixture, { - ...defaultProps, - variant: createBoundProperty('obscured' as const), - }); - expect(component.inputType()).toBe('password'); - - setComponentProps(fixture, { - ...defaultProps, - variant: createBoundProperty('number' as const), - }); - expect(component.inputType()).toBe('number'); - }); - - it('should call onUpdate when input changes', () => { - fixture.detectChanges(); - const input = fixture.debugElement.query(By.css('input')); - input.nativeElement.value = 'newuser'; - input.triggerEventHandler('input', {target: input.nativeElement}); - - expect(component.props()['value']!.onUpdate).toHaveBeenCalledWith('newuser'); - }); - - it('should show error messages when checks fail', async () => { - const isValidProp = createBoundProperty(true); - const errorsProp = createBoundProperty([]); - - setComponentProps(fixture, { - ...defaultProps, - isValid: isValidProp, - validationErrors: errorsProp, - }); - - fixture.detectChanges(); - - const errorMsgBefore = fixture.debugElement.query(By.css('.a2ui-error-message')); - expect(errorMsgBefore).toBeFalsy(); - - isValidProp.value.set(false); - errorsProp.value.set(['Value is required']); - fixture.detectChanges(); - - const errorMsgAfter = fixture.debugElement.query(By.css('.a2ui-error-message')); - expect(errorMsgAfter).toBeTruthy(); - expect(errorMsgAfter.nativeElement.textContent).toContain('Value is required'); - }); - - it('should handle multiple error messages', () => { - setComponentProps(fixture, { - ...defaultProps, - isValid: createBoundProperty(false), - validationErrors: createBoundProperty(['Error 1', 'Error 2']), - }); - - fixture.detectChanges(); - - const errorMsgs = fixture.debugElement.queryAll(By.css('.a2ui-error-message')); - expect(errorMsgs.length).toBe(2); - expect(errorMsgs[0].nativeElement.textContent).toContain('Error 1'); - expect(errorMsgs[1].nativeElement.textContent).toContain('Error 2'); - }); -}); diff --git a/renderers/angular/src/v0_9/catalog/basic/text-field.component.ts b/renderers/angular/src/v0_9/catalog/basic/text-field.component.ts deleted file mode 100644 index a56cdf64ac..0000000000 --- a/renderers/angular/src/v0_9/catalog/basic/text-field.component.ts +++ /dev/null @@ -1,118 +0,0 @@ -/** - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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, ChangeDetectionStrategy} from '@angular/core'; -import {BasicCatalogComponent} from './basic-catalog-component'; -import {TextFieldApi} from '@a2ui/web_core/v0_9/basic_catalog'; - -/** - * Angular implementation of the A2UI TextField component (v0.9). - * - * Renders a text input field with an optional label. - * Updates the bound data model property on every input change. - * - * Supported CSS variables: - * - `--a2ui-color-input`: Controls the background color of the input. - * - `--a2ui-color-on-input`: Controls the text color of the input. - * - `--a2ui-textfield-border`: Controls the border of the input. - * - `--a2ui-textfield-border-radius`: Controls the border radius of the input. - * - `--a2ui-textfield-padding`: Controls the padding of the input. - * - `--a2ui-textfield-color-border-focus`: Controls the border color on focus. - * - `--a2ui-textfield-color-error`: Controls the border and text color for error states. - * - `--a2ui-textfield-label-font-size`: Controls the font size of the label. - * - `--a2ui-textfield-label-font-weight`: Controls the font weight of the label. - */ -@Component({ - selector: 'a2ui-v09-text-field', - standalone: true, - imports: [], - template: ` -
- @if (label()) { - - } - - @for (message of props()['validationErrors']?.value(); track message) { -
{{ message }}
- } -
- `, - styles: [ - ` - .a2ui-text-field-container { - display: flex; - flex-direction: column; - gap: var(--a2ui-spacing-xs, 4px); - margin: var(--a2ui-spacing-xs, 4px); - } - label { - font-size: var( - --a2ui-textfield-label-font-size, - var(--a2ui-label-font-size, var(--a2ui-font-size-s, 14px)) - ); - font-weight: var(--a2ui-textfield-label-font-weight, bold); - color: var(--a2ui-text-color-text, var(--a2ui-color-on-background, #333)); - } - input { - padding: var(--a2ui-textfield-padding, 8px); - border: var(--a2ui-textfield-border, 1px solid var(--a2ui-color-border, #ccc)); - border-radius: var(--a2ui-textfield-border-radius, 4px); - background-color: var(--a2ui-color-input, #fff); - color: var(--a2ui-color-on-input, #333); - } - input:focus { - border-color: var(--a2ui-textfield-color-border-focus, var(--a2ui-color-primary, #17e)); - outline: none; - } - input.invalid { - border-color: var(--a2ui-textfield-color-error, var(--a2ui-color-error, red)); - } - .a2ui-error-message { - color: var(--a2ui-textfield-color-error, var(--a2ui-color-error, red)); - font-size: var(--a2ui-font-size-xs, 12px); - } - `, - ], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class TextFieldComponent extends BasicCatalogComponent { - readonly label = computed(() => this.props()['label']?.value()); - readonly value = computed(() => this.props()['value']?.value() || ''); - readonly variant = computed(() => this.props()['variant']?.value()); - - readonly inputType = computed(() => { - switch (this.variant()) { - case 'obscured': - return 'password'; - case 'number': - return 'number'; - default: - return 'text'; - } - }); - - handleInput(event: Event) { - const value = (event.target as HTMLInputElement).value; - // Update the data path. If anything is listening to this path, it will be - // notified. - this.props()['value']?.onUpdate(value); - } -} diff --git a/renderers/angular/src/v0_9/catalog/basic/text.component.spec.ts b/renderers/angular/src/v0_9/catalog/basic/text.component.spec.ts deleted file mode 100644 index 56bf80663a..0000000000 --- a/renderers/angular/src/v0_9/catalog/basic/text.component.spec.ts +++ /dev/null @@ -1,181 +0,0 @@ -/** - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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 {ComponentFixture, TestBed} from '@angular/core/testing'; -import {TextComponent} from './text.component'; -import {By} from '@angular/platform-browser'; -import {MarkdownRenderer} from '../../core/markdown'; -import {setComponentProps, createBoundProperty, ComponentToProps} from '@a2ui/angular/testing'; -import {A2uiRendererService, A2UI_RENDERER_CONFIG} from '../../core/a2ui-renderer.service'; - -describe('TextComponent', () => { - let component: TextComponent; - let fixture: ComponentFixture; - let mockMarkdownRenderer: jasmine.SpyObj; - let defaultProps: ComponentToProps; - - beforeEach(async () => { - mockMarkdownRenderer = jasmine.createSpyObj('MarkdownRenderer', ['render']); - mockMarkdownRenderer.render.and.callFake((text: string) => Promise.resolve(`

${text}

`)); - - await TestBed.configureTestingModule({ - imports: [TextComponent], - providers: [ - {provide: MarkdownRenderer, useValue: mockMarkdownRenderer}, - A2uiRendererService, - {provide: A2UI_RENDERER_CONFIG, useValue: {catalogs: []}}, - ], - }).compileComponents(); - - fixture = TestBed.createComponent(TextComponent); - component = fixture.componentInstance; - fixture.componentRef.setInput('surfaceId', 'surf1'); - - defaultProps = { - text: createBoundProperty('Hello World'), - }; - setComponentProps(fixture, defaultProps); - }); - - it('should create', () => { - fixture.detectChanges(); - expect(component).toBeTruthy(); - }); - - it('should render the markdown text', async () => { - fixture.detectChanges(); - await fixture.whenStable(); - fixture.detectChanges(); - - const element = fixture.debugElement.query(By.css('.a2ui-text')); - expect(element).toBeTruthy(); - const p = element.query(By.css('p')); - expect(p).toBeTruthy(); - expect(p.nativeElement.textContent.trim()).toBe('Hello World'); - expect(mockMarkdownRenderer.render).toHaveBeenCalledWith('Hello World'); - }); - - it('should handle variant h1', async () => { - setComponentProps(fixture, { - text: createBoundProperty('Heading'), - variant: createBoundProperty('h1' as const), - }); - fixture.detectChanges(); - await fixture.whenStable(); - fixture.detectChanges(); - - expect(mockMarkdownRenderer.render).not.toHaveBeenCalled(); - const element = fixture.debugElement.query(By.css('.a2ui-text.h1')); - expect(element).toBeTruthy(); - const h1 = element.query(By.css('h1')); - expect(h1).toBeTruthy(); - expect(h1.nativeElement.textContent.trim()).toBe('Heading'); - }); - - it('should handle variant caption', async () => { - setComponentProps(fixture, { - text: createBoundProperty('Caption'), - variant: createBoundProperty('caption' as const), - }); - fixture.detectChanges(); - await fixture.whenStable(); - fixture.detectChanges(); - - expect(mockMarkdownRenderer.render).not.toHaveBeenCalled(); - const element = fixture.debugElement.query(By.css('.a2ui-text.caption')); - expect(element).toBeTruthy(); - const em = element.query(By.css('em')); - expect(em).toBeTruthy(); - expect(em.nativeElement.textContent.trim()).toBe('Caption'); - }); - - it('should handle variant h2', async () => { - setComponentProps(fixture, { - text: createBoundProperty('Heading'), - variant: createBoundProperty('h2' as const), - }); - fixture.detectChanges(); - await fixture.whenStable(); - fixture.detectChanges(); - - expect(mockMarkdownRenderer.render).not.toHaveBeenCalled(); - const element = fixture.debugElement.query(By.css('.a2ui-text.h2')); - expect(element).toBeTruthy(); - const h2 = element.query(By.css('h2')); - expect(h2).toBeTruthy(); - expect(h2.nativeElement.textContent.trim()).toBe('Heading'); - }); - - it('should handle variant h3', async () => { - setComponentProps(fixture, { - text: createBoundProperty('Heading'), - variant: createBoundProperty('h3' as const), - }); - fixture.detectChanges(); - await fixture.whenStable(); - fixture.detectChanges(); - - expect(mockMarkdownRenderer.render).not.toHaveBeenCalled(); - const element = fixture.debugElement.query(By.css('.a2ui-text.h3')); - expect(element).toBeTruthy(); - const h3 = element.query(By.css('h3')); - expect(h3).toBeTruthy(); - expect(h3.nativeElement.textContent.trim()).toBe('Heading'); - }); - - it('should handle variant h4', async () => { - setComponentProps(fixture, { - text: createBoundProperty('Heading'), - variant: createBoundProperty('h4' as const), - }); - fixture.detectChanges(); - await fixture.whenStable(); - fixture.detectChanges(); - - expect(mockMarkdownRenderer.render).not.toHaveBeenCalled(); - const element = fixture.debugElement.query(By.css('.a2ui-text.h4')); - expect(element).toBeTruthy(); - const h4 = element.query(By.css('h4')); - expect(h4).toBeTruthy(); - expect(h4.nativeElement.textContent.trim()).toBe('Heading'); - }); - - it('should handle variant h5', async () => { - setComponentProps(fixture, { - text: createBoundProperty('Heading'), - variant: createBoundProperty('h5' as const), - }); - fixture.detectChanges(); - await fixture.whenStable(); - fixture.detectChanges(); - - expect(mockMarkdownRenderer.render).not.toHaveBeenCalled(); - const element = fixture.debugElement.query(By.css('.a2ui-text.h5')); - expect(element).toBeTruthy(); - const h5 = element.query(By.css('h5')); - expect(h5).toBeTruthy(); - expect(h5.nativeElement.textContent.trim()).toBe('Heading'); - }); - - it('should handle missing text property', async () => { - setComponentProps(fixture, {} as ComponentToProps); - fixture.detectChanges(); - await fixture.whenStable(); - fixture.detectChanges(); - - expect(mockMarkdownRenderer.render).toHaveBeenCalledWith(''); - }); -}); diff --git a/renderers/angular/src/v0_9/catalog/basic/text.component.ts b/renderers/angular/src/v0_9/catalog/basic/text.component.ts deleted file mode 100644 index 3b8599c075..0000000000 --- a/renderers/angular/src/v0_9/catalog/basic/text.component.ts +++ /dev/null @@ -1,172 +0,0 @@ -/** - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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, ChangeDetectionStrategy, inject, signal, effect} from '@angular/core'; -import {MarkdownRenderer} from '../../core/markdown'; -import {BasicCatalogComponent} from './basic-catalog-component'; -import {TextApi} from '@a2ui/web_core/v0_9/basic_catalog'; - -/** - * Angular implementation of the A2UI Text component (v0.9). - * - * Renders text with support for simple Markdown. - * - * Supported CSS variables: - * - `--a2ui-text-color-text`: Controls the text color. - * - `--a2ui-text-margin`: Controls the margin of text elements. - * - `--a2ui-font-family-title`: Controls the font family for titles. - * - `--a2ui-line-height-headings`: Controls the line height for headings. - * - `--a2ui-line-height-body`: Controls the line height for body text. - * - `--a2ui-text-caption-color`: Controls the color for caption text. - * - `--a2ui-text-a-color`: Controls the color for links. - * - `--a2ui-text-a-font-weight`: Controls the font weight for links. - * - Font sizes: `--a2ui-font-size-2xl`, `--a2ui-font-size-xl`, `--a2ui-font-size-l`, `--a2ui-font-size-m`, `--a2ui-font-size-s`, `--a2ui-font-size-xs`. - */ -@Component({ - selector: 'a2ui-v09-text', - standalone: true, - template: ` - @if (isNonMarkdownVariant()) { - - - @switch (variant()) { - @case ('h1') { -

{{ text() }}

- } - @case ('h2') { -

{{ text() }}

- } - @case ('h3') { -

{{ text() }}

- } - @case ('h4') { -

{{ text() }}

- } - @case ('h5') { -
{{ text() }}
- } - @case ('caption') { - {{ text() }} - } - } -
- } @else { - - } - `, - // We use :host ::ng-deep because the template content is injected via innerHTML (Markdown). - // Angular's default view encapsulation cannot target elements injected via innerHTML because they lack the scoping attributes generated at compile time. - // ::ng-deep allows styles to reach into the injected HTML, while :host keeps them scoped to this component. - styles: [ - ` - :host ::ng-deep .a2ui-text p, - :host ::ng-deep .a2ui-text h1, - :host ::ng-deep .a2ui-text h2, - :host ::ng-deep .a2ui-text h3, - :host ::ng-deep .a2ui-text h4, - :host ::ng-deep .a2ui-text h5, - :host ::ng-deep .a2ui-text h6, - :host ::ng-deep .a2ui-text ol, - :host ::ng-deep .a2ui-text ul, - :host ::ng-deep .a2ui-text li, - :host ::ng-deep .a2ui-text blockquote, - :host ::ng-deep .a2ui-text pre { - margin: var(--_a2ui-text-margin, 0); - } - :host ::ng-deep .a2ui-text { - color: var( - --_a2ui-text-color, - var(--a2ui-text-color-text, var(--a2ui-color-on-background)) - ); - } - :host ::ng-deep .a2ui-text h1, - :host ::ng-deep .a2ui-text h2, - :host ::ng-deep .a2ui-text h3, - :host ::ng-deep .a2ui-text h4, - :host ::ng-deep .a2ui-text h5, - :host ::ng-deep .a2ui-text h6 { - font-family: var(--a2ui-font-family-title, inherit); - line-height: var(--a2ui-line-height-headings, 1.2); - } - :host ::ng-deep .a2ui-text h1 { - font-size: var(--a2ui-font-size-2xl); - } - :host ::ng-deep .a2ui-text h2 { - font-size: var(--a2ui-font-size-xl); - } - :host ::ng-deep .a2ui-text h3 { - font-size: var(--a2ui-font-size-l); - } - :host ::ng-deep .a2ui-text p, - :host ::ng-deep .a2ui-text h4 { - font-size: var(--a2ui-font-size-m); - } - :host ::ng-deep .a2ui-text h5 { - font-size: var(--a2ui-font-size-s); - } - :host ::ng-deep .a2ui-text p { - line-height: var(--a2ui-line-height-body, 1.5); - } - :host ::ng-deep .a2ui-text.caption { - font-size: var(--a2ui-font-size-xs); - color: var(--a2ui-text-caption-color, light-dark(#666, #aaa)); - } - :host ::ng-deep .a2ui-text a { - color: var(--a2ui-text-a-color, inherit); - font-weight: var(--a2ui-text-a-font-weight, inherit); - } - `, - ], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class TextComponent extends BasicCatalogComponent { - private markdownRenderer = inject(MarkdownRenderer); - - private static readonly NON_MARKDOWN_VARIANTS = new Set([ - 'h1', - 'h2', - 'h3', - 'h4', - 'h5', - 'caption', - ]); - - readonly variant = computed(() => this.props()['variant']?.value() || 'body'); - readonly text = computed(() => this.props()['text']?.value() || ''); - - readonly isNonMarkdownVariant = computed(() => { - return TextComponent.NON_MARKDOWN_VARIANTS.has(this.variant()); - }); - - resolvedText = signal(''); - private renderRequestId = 0; - - constructor() { - super(); - effect(() => { - if (this.isNonMarkdownVariant()) { - return; - } - const text = this.text(); - const requestId = ++this.renderRequestId; - this.markdownRenderer.render(text).then(rendered => { - if (requestId === this.renderRequestId) { - this.resolvedText.set(rendered); - } - }); - }); - } -} diff --git a/renderers/angular/src/v0_9/catalog/basic/utils.ts b/renderers/angular/src/v0_9/catalog/basic/utils.ts deleted file mode 100644 index a465133624..0000000000 --- a/renderers/angular/src/v0_9/catalog/basic/utils.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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. - */ - -/** - * Maps A2UI justification values to CSS justify-content values. - */ -export const JUSTIFY_MAP: Record = { - start: 'flex-start', - center: 'center', - end: 'flex-end', - spaceBetween: 'space-between', - spaceAround: 'space-around', - spaceEvenly: 'space-evenly', - stretch: 'stretch', -}; - -/** - * Maps A2UI alignment values to CSS align-items values. - */ -export const ALIGN_MAP: Record = { - start: 'flex-start', - center: 'center', - end: 'flex-end', - stretch: 'stretch', - baseline: 'baseline', -}; diff --git a/renderers/angular/src/v0_9/catalog/basic/video.component.spec.ts b/renderers/angular/src/v0_9/catalog/basic/video.component.spec.ts deleted file mode 100644 index af9535c154..0000000000 --- a/renderers/angular/src/v0_9/catalog/basic/video.component.spec.ts +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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 {ComponentFixture, TestBed} from '@angular/core/testing'; -import {VideoComponent} from './video.component'; -import {A2uiRendererService} from '../../core/a2ui-renderer.service'; -import {ComponentBinder} from '../../core/component-binder.service'; -import {setComponentProps, createBoundProperty, ComponentToProps} from '@a2ui/angular/testing'; - -describe('VideoComponent', () => { - let component: VideoComponent; - let fixture: ComponentFixture; - let defaultProps: ComponentToProps; - - beforeEach(async () => { - const mockRendererService = { - surfaceGroup: { - getSurface: jasmine.createSpy('getSurface').and.returnValue({ - componentsModel: new Map(), - catalog: { - id: 'mock-catalog', - components: new Map(), - }, - }), - }, - }; - const mockBinder = jasmine.createSpyObj('ComponentBinder', ['bind']); - - await TestBed.configureTestingModule({ - imports: [VideoComponent], - providers: [ - {provide: A2uiRendererService, useValue: mockRendererService}, - {provide: ComponentBinder, useValue: mockBinder}, - ], - }).compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(VideoComponent); - component = fixture.componentInstance; - fixture.componentRef.setInput('surfaceId', 'test-surface'); - fixture.componentRef.setInput('dataContextPath', '/'); - - defaultProps = { - url: createBoundProperty('https://example.com/video.mp4'), - }; - setComponentProps(fixture, defaultProps); - }); - - it('should create', () => { - fixture.detectChanges(); - expect(component).toBeTruthy(); - }); - - it('should render video with url', () => { - fixture.detectChanges(); - const video = fixture.nativeElement.querySelector('video') as HTMLVideoElement; - expect(video.src).toBeTruthy(); - }); - - it('should handle missing props', () => { - setComponentProps(fixture, {} as ComponentToProps); - fixture.detectChanges(); - const video = fixture.nativeElement.querySelector('video') as HTMLVideoElement; - expect(video.getAttribute('src')).toBeFalsy(); - }); -}); diff --git a/renderers/angular/src/v0_9/catalog/basic/video.component.ts b/renderers/angular/src/v0_9/catalog/basic/video.component.ts deleted file mode 100644 index cf120f7f2a..0000000000 --- a/renderers/angular/src/v0_9/catalog/basic/video.component.ts +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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, ChangeDetectionStrategy} from '@angular/core'; -import {BasicCatalogComponent} from './basic-catalog-component'; -import {VideoApi} from '@a2ui/web_core/v0_9/basic_catalog'; - -/** - * Angular implementation of the A2UI Video component (v0.9). - * - * Renders a video player with standard controls. - * - * Supported CSS variables: - * - `--a2ui-video-border-radius`: Controls the border radius of the video element. - */ -@Component({ - selector: 'a2ui-v09-video', - standalone: true, - imports: [], - template: ` -
- -
- `, - styles: [ - ` - .a2ui-video-container { - width: 100%; - max-width: 100%; - } - .a2ui-video { - width: 100%; - height: auto; - display: block; - border-radius: var(--a2ui-video-border-radius, 0); - } - `, - ], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class VideoComponent extends BasicCatalogComponent { - readonly url = computed(() => this.props()['url']?.value()); -}