From 531b5f8a799b4fd9db04ab3b1c6ace4df0e1f140 Mon Sep 17 00:00:00 2001 From: ppazosp Date: Sun, 29 Mar 2026 23:17:07 +0200 Subject: [PATCH 1/5] refactor[web_core]: centralize layout mapping logic (fixes #840) --- .../v0_9/catalog/basic/column.component.ts | 6 +- .../src/v0_9/catalog/basic/row.component.ts | 5 +- .../catalogs/minimal/components/Column.ts | 37 +-------- .../v0_9/catalogs/minimal/components/Row.ts | 37 +-------- .../react/src/v0_9/catalog/basic/utils.ts | 36 +-------- .../catalog/minimal/components/Column.tsx | 38 +--------- .../v0_9/catalog/minimal/components/Row.tsx | 38 +--------- renderers/web_core/src/v0_9/common/layout.ts | 75 +++++++++++++++++++ renderers/web_core/src/v0_9/index.ts | 1 + 9 files changed, 87 insertions(+), 186 deletions(-) create mode 100644 renderers/web_core/src/v0_9/common/layout.ts diff --git a/renderers/angular/src/v0_9/catalog/basic/column.component.ts b/renderers/angular/src/v0_9/catalog/basic/column.component.ts index 2bae36f54..5a1552c30 100644 --- a/renderers/angular/src/v0_9/catalog/basic/column.component.ts +++ b/renderers/angular/src/v0_9/catalog/basic/column.component.ts @@ -17,7 +17,7 @@ import { Component, input, computed, ChangeDetectionStrategy } from '@angular/core'; import { ComponentHostComponent } from '../../core/component-host.component'; import { BoundProperty } from '../../core/types'; - +import { mapJustify, mapAlign } from '@a2ui/web_core/v0_9'; import { getNormalizedPath } from '../../core/utils'; /** @@ -76,8 +76,8 @@ export class ColumnComponent { componentId = input(); dataContextPath = input('/'); - protected justify = computed(() => this.props()['justify']?.value()); - protected align = computed(() => this.props()['align']?.value()); + protected justify = computed(() => mapJustify(this.props()['justify']?.value())); + protected align = computed(() => mapAlign(this.props()['align']?.value())); protected children = computed(() => { const raw = this.props()['children']?.value() || []; diff --git a/renderers/angular/src/v0_9/catalog/basic/row.component.ts b/renderers/angular/src/v0_9/catalog/basic/row.component.ts index d4fc623ff..fe5c0796c 100644 --- a/renderers/angular/src/v0_9/catalog/basic/row.component.ts +++ b/renderers/angular/src/v0_9/catalog/basic/row.component.ts @@ -17,6 +17,7 @@ import { Component, input, computed, ChangeDetectionStrategy } from '@angular/core'; import { ComponentHostComponent } from '../../core/component-host.component'; import { BoundProperty } from '../../core/types'; +import { mapJustify, mapAlign } from '@a2ui/web_core/v0_9'; import { getNormalizedPath } from '../../core/utils'; /** @@ -75,8 +76,8 @@ export class RowComponent { componentId = input(); dataContextPath = input('/'); - protected justify = computed(() => this.props()['justify']?.value()); - protected align = computed(() => this.props()['align']?.value()); + protected justify = computed(() => mapJustify(this.props()['justify']?.value())); + protected align = computed(() => mapAlign(this.props()['align']?.value())); protected children = computed(() => { const raw = this.props()['children']?.value() || []; diff --git a/renderers/lit/src/v0_9/catalogs/minimal/components/Column.ts b/renderers/lit/src/v0_9/catalogs/minimal/components/Column.ts index 2ddadec18..5fe43c052 100644 --- a/renderers/lit/src/v0_9/catalogs/minimal/components/Column.ts +++ b/renderers/lit/src/v0_9/catalogs/minimal/components/Column.ts @@ -19,44 +19,9 @@ import { customElement } from "lit/decorators.js"; import { map } from "lit/directives/map.js"; import { styleMap } from "lit/directives/style-map.js"; import { ColumnApi } from "@a2ui/web_core/v0_9/basic_catalog"; +import { mapJustify, mapAlign } from "@a2ui/web_core/v0_9"; import { A2uiLitElement, A2uiController } from "@a2ui/lit/v0_9"; -function mapJustify(justify: string | undefined): string { - switch (justify) { - case "start": - return "flex-start"; - case "center": - return "center"; - case "end": - return "flex-end"; - case "spaceBetween": - return "space-between"; - case "spaceAround": - return "space-around"; - case "spaceEvenly": - return "space-evenly"; - case "stretch": - return "stretch"; - default: - return "flex-start"; - } -} - -function mapAlign(align: string | undefined): string { - switch (align) { - case "start": - return "flex-start"; - case "center": - return "center"; - case "end": - return "flex-end"; - case "stretch": - return "stretch"; - default: - return "stretch"; - } -} - @customElement("a2ui-column") export class A2uiColumnElement extends A2uiLitElement { protected createController() { diff --git a/renderers/lit/src/v0_9/catalogs/minimal/components/Row.ts b/renderers/lit/src/v0_9/catalogs/minimal/components/Row.ts index 541bf0bda..470b56370 100644 --- a/renderers/lit/src/v0_9/catalogs/minimal/components/Row.ts +++ b/renderers/lit/src/v0_9/catalogs/minimal/components/Row.ts @@ -19,44 +19,9 @@ import { customElement } from "lit/decorators.js"; import { map } from "lit/directives/map.js"; import { styleMap } from "lit/directives/style-map.js"; import { RowApi } from "@a2ui/web_core/v0_9/basic_catalog"; +import { mapJustify, mapAlign } from "@a2ui/web_core/v0_9"; import { A2uiLitElement, A2uiController } from "@a2ui/lit/v0_9"; -function mapJustify(justify: string | undefined): string { - switch (justify) { - case "start": - return "flex-start"; - case "center": - return "center"; - case "end": - return "flex-end"; - case "spaceBetween": - return "space-between"; - case "spaceAround": - return "space-around"; - case "spaceEvenly": - return "space-evenly"; - case "stretch": - return "stretch"; - default: - return "flex-start"; - } -} - -function mapAlign(align: string | undefined): string { - switch (align) { - case "start": - return "flex-start"; - case "center": - return "center"; - case "end": - return "flex-end"; - case "stretch": - return "stretch"; - default: - return "stretch"; - } -} - @customElement("a2ui-row") export class A2uiRowElement extends A2uiLitElement { protected createController() { diff --git a/renderers/react/src/v0_9/catalog/basic/utils.ts b/renderers/react/src/v0_9/catalog/basic/utils.ts index 375bac00d..692e8b359 100644 --- a/renderers/react/src/v0_9/catalog/basic/utils.ts +++ b/renderers/react/src/v0_9/catalog/basic/utils.ts @@ -28,41 +28,7 @@ export const STANDARD_BORDER = '1px solid #ccc'; /** Standard border radius. */ export const STANDARD_RADIUS = '8px'; -export const mapJustify = (j?: string) => { - switch (j) { - case 'center': - return 'center'; - case 'end': - return 'flex-end'; - case 'spaceAround': - return 'space-around'; - case 'spaceBetween': - return 'space-between'; - case 'spaceEvenly': - return 'space-evenly'; - case 'start': - return 'flex-start'; - case 'stretch': - return 'stretch'; - default: - return 'flex-start'; - } -}; - -export const mapAlign = (a?: string) => { - switch (a) { - case 'start': - return 'flex-start'; - case 'center': - return 'center'; - case 'end': - return 'flex-end'; - case 'stretch': - return 'stretch'; - default: - return 'stretch'; - } -}; +export {mapJustify, mapAlign} from '@a2ui/web_core/v0_9'; export const getBaseLeafStyle = (): React.CSSProperties => ({ margin: LEAF_MARGIN, diff --git a/renderers/react/src/v0_9/catalog/minimal/components/Column.tsx b/renderers/react/src/v0_9/catalog/minimal/components/Column.tsx index b137f372c..f130af8b1 100644 --- a/renderers/react/src/v0_9/catalog/minimal/components/Column.tsx +++ b/renderers/react/src/v0_9/catalog/minimal/components/Column.tsx @@ -16,7 +16,7 @@ import {createReactComponent} from '../../../adapter'; import {z} from 'zod'; -import {CommonSchemas} from '@a2ui/web_core/v0_9'; +import {CommonSchemas, mapJustify, mapAlign} from '@a2ui/web_core/v0_9'; import {ChildList} from './ChildList'; export const ColumnSchema = z.object({ @@ -27,42 +27,6 @@ export const ColumnSchema = z.object({ align: z.enum(['center', 'end', 'start', 'stretch']).optional(), }); -const mapJustify = (j?: string) => { - switch (j) { - case 'center': - return 'center'; - case 'end': - return 'flex-end'; - case 'spaceAround': - return 'space-around'; - case 'spaceBetween': - return 'space-between'; - case 'spaceEvenly': - return 'space-evenly'; - case 'start': - return 'flex-start'; - case 'stretch': - return 'stretch'; - default: - return 'flex-start'; - } -}; - -const mapAlign = (a?: string) => { - switch (a) { - case 'start': - return 'flex-start'; - case 'center': - return 'center'; - case 'end': - return 'flex-end'; - case 'stretch': - return 'stretch'; - default: - return 'stretch'; - } -}; - export const ColumnApiDef = { name: 'Column', schema: ColumnSchema, diff --git a/renderers/react/src/v0_9/catalog/minimal/components/Row.tsx b/renderers/react/src/v0_9/catalog/minimal/components/Row.tsx index 7e8cd9bad..34e1ee765 100644 --- a/renderers/react/src/v0_9/catalog/minimal/components/Row.tsx +++ b/renderers/react/src/v0_9/catalog/minimal/components/Row.tsx @@ -16,7 +16,7 @@ import {createReactComponent} from '../../../adapter'; import {z} from 'zod'; -import {CommonSchemas} from '@a2ui/web_core/v0_9'; +import {CommonSchemas, mapJustify, mapAlign} from '@a2ui/web_core/v0_9'; import {ChildList} from './ChildList'; export const RowSchema = z.object({ @@ -27,42 +27,6 @@ export const RowSchema = z.object({ align: z.enum(['start', 'center', 'end', 'stretch']).optional(), }); -const mapJustify = (j?: string) => { - switch (j) { - case 'center': - return 'center'; - case 'end': - return 'flex-end'; - case 'spaceAround': - return 'space-around'; - case 'spaceBetween': - return 'space-between'; - case 'spaceEvenly': - return 'space-evenly'; - case 'start': - return 'flex-start'; - case 'stretch': - return 'stretch'; - default: - return 'flex-start'; - } -}; - -const mapAlign = (a?: string) => { - switch (a) { - case 'start': - return 'flex-start'; - case 'center': - return 'center'; - case 'end': - return 'flex-end'; - case 'stretch': - return 'stretch'; - default: - return 'stretch'; - } -}; - export const RowApiDef = { name: 'Row', schema: RowSchema, diff --git a/renderers/web_core/src/v0_9/common/layout.ts b/renderers/web_core/src/v0_9/common/layout.ts new file mode 100644 index 000000000..97e6ceca1 --- /dev/null +++ b/renderers/web_core/src/v0_9/common/layout.ts @@ -0,0 +1,75 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Centralized layout mapping utilities for Web/CSS-based renderers. + * + * Maps A2UI layout enum values (e.g., `spaceBetween`) to their corresponding + * CSS values (e.g., `space-between`). These functions are shared across all + * web renderers (React, Lit, Angular) to ensure consistent behavior. + */ + +/** + * Maps an A2UI justify enum value to its CSS `justify-content` equivalent. + * + * @param value - An A2UI justify value such as `'start'`, `'center'`, + * `'spaceBetween'`, etc. + * @returns The corresponding CSS `justify-content` value. Defaults to + * `'flex-start'` for unrecognized or undefined input. + */ +export function mapJustify(value?: string): string { + switch (value) { + case 'center': + return 'center'; + case 'end': + return 'flex-end'; + case 'spaceAround': + return 'space-around'; + case 'spaceBetween': + return 'space-between'; + case 'spaceEvenly': + return 'space-evenly'; + case 'start': + return 'flex-start'; + case 'stretch': + return 'stretch'; + default: + return 'flex-start'; + } +} + +/** + * Maps an A2UI align enum value to its CSS `align-items` equivalent. + * + * @param value - An A2UI align value such as `'start'`, `'center'`, `'end'`, + * or `'stretch'`. + * @returns The corresponding CSS `align-items` value. Defaults to `'stretch'` + * for unrecognized or undefined input. + */ +export function mapAlign(value?: string): string { + switch (value) { + case 'start': + return 'flex-start'; + case 'center': + return 'center'; + case 'end': + return 'flex-end'; + case 'stretch': + return 'stretch'; + default: + return 'stretch'; + } +} diff --git a/renderers/web_core/src/v0_9/index.ts b/renderers/web_core/src/v0_9/index.ts index 51be3ccbb..b7a9bbdcd 100644 --- a/renderers/web_core/src/v0_9/index.ts +++ b/renderers/web_core/src/v0_9/index.ts @@ -24,6 +24,7 @@ export * from './catalog/function_invoker.js'; export * from './catalog/types.js'; export * from './common/events.js'; +export * from './common/layout.js'; export * from './processing/message-processor.js'; export * from './rendering/component-context.js'; export * from './rendering/data-context.js'; From 470fe86ee6779277d5e187e2ee6636d3391f7d49 Mon Sep 17 00:00:00 2001 From: ppazosp Date: Sun, 29 Mar 2026 23:36:31 +0200 Subject: [PATCH 2/5] refactor[web_core]: use object maps for layout + add tests per review --- .../web_core/src/v0_9/common/layout.test.ts | 70 +++++++++++++++++++ renderers/web_core/src/v0_9/common/layout.ts | 49 +++++-------- 2 files changed, 89 insertions(+), 30 deletions(-) create mode 100644 renderers/web_core/src/v0_9/common/layout.test.ts diff --git a/renderers/web_core/src/v0_9/common/layout.test.ts b/renderers/web_core/src/v0_9/common/layout.test.ts new file mode 100644 index 000000000..7de8a233e --- /dev/null +++ b/renderers/web_core/src/v0_9/common/layout.test.ts @@ -0,0 +1,70 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import assert from 'node:assert'; +import {describe, it} from 'node:test'; +import {mapAlign, mapJustify} from './layout.js'; + +describe('mapJustify', () => { + it('maps center to center', () => { + assert.strictEqual(mapJustify('center'), 'center'); + }); + it('maps end to flex-end', () => { + assert.strictEqual(mapJustify('end'), 'flex-end'); + }); + it('maps spaceAround to space-around', () => { + assert.strictEqual(mapJustify('spaceAround'), 'space-around'); + }); + it('maps spaceBetween to space-between', () => { + assert.strictEqual(mapJustify('spaceBetween'), 'space-between'); + }); + it('maps spaceEvenly to space-evenly', () => { + assert.strictEqual(mapJustify('spaceEvenly'), 'space-evenly'); + }); + it('maps start to flex-start', () => { + assert.strictEqual(mapJustify('start'), 'flex-start'); + }); + it('maps stretch to stretch', () => { + assert.strictEqual(mapJustify('stretch'), 'stretch'); + }); + it('returns flex-start for undefined input', () => { + assert.strictEqual(mapJustify(undefined), 'flex-start'); + }); + it('returns flex-start for unknown string input', () => { + assert.strictEqual(mapJustify('unknown'), 'flex-start'); + }); +}); + +describe('mapAlign', () => { + it('maps center to center', () => { + assert.strictEqual(mapAlign('center'), 'center'); + }); + it('maps end to flex-end', () => { + assert.strictEqual(mapAlign('end'), 'flex-end'); + }); + it('maps start to flex-start', () => { + assert.strictEqual(mapAlign('start'), 'flex-start'); + }); + it('maps stretch to stretch', () => { + assert.strictEqual(mapAlign('stretch'), 'stretch'); + }); + it('returns stretch for undefined input', () => { + assert.strictEqual(mapAlign(undefined), 'stretch'); + }); + it('returns stretch for unknown string input', () => { + assert.strictEqual(mapAlign('unknown'), 'stretch'); + }); +}); diff --git a/renderers/web_core/src/v0_9/common/layout.ts b/renderers/web_core/src/v0_9/common/layout.ts index 97e6ceca1..90a52b6f0 100644 --- a/renderers/web_core/src/v0_9/common/layout.ts +++ b/renderers/web_core/src/v0_9/common/layout.ts @@ -30,25 +30,18 @@ * @returns The corresponding CSS `justify-content` value. Defaults to * `'flex-start'` for unrecognized or undefined input. */ +const justifyMap: Record = { + center: 'center', + end: 'flex-end', + spaceAround: 'space-around', + spaceBetween: 'space-between', + spaceEvenly: 'space-evenly', + start: 'flex-start', + stretch: 'stretch', +}; + export function mapJustify(value?: string): string { - switch (value) { - case 'center': - return 'center'; - case 'end': - return 'flex-end'; - case 'spaceAround': - return 'space-around'; - case 'spaceBetween': - return 'space-between'; - case 'spaceEvenly': - return 'space-evenly'; - case 'start': - return 'flex-start'; - case 'stretch': - return 'stretch'; - default: - return 'flex-start'; - } + return (value && justifyMap[value]) || 'flex-start'; } /** @@ -59,17 +52,13 @@ export function mapJustify(value?: string): string { * @returns The corresponding CSS `align-items` value. Defaults to `'stretch'` * for unrecognized or undefined input. */ +const alignMap: Record = { + center: 'center', + end: 'flex-end', + start: 'flex-start', + stretch: 'stretch', +}; + export function mapAlign(value?: string): string { - switch (value) { - case 'start': - return 'flex-start'; - case 'center': - return 'center'; - case 'end': - return 'flex-end'; - case 'stretch': - return 'stretch'; - default: - return 'stretch'; - } + return (value && alignMap[value]) || 'stretch'; } From 141657866cf5c33bdcd73f4ba63f53f47f716a67 Mon Sep 17 00:00:00 2001 From: ppazosp Date: Sat, 18 Apr 2026 12:10:22 +0200 Subject: [PATCH 3/5] refactor[web_core]: move layout helpers to basic_catalog/styles per review Moved mapJustify/mapAlign from common/ to basic_catalog/styles/ since they are specific to basic catalog widgets (Row, Column, List), not protocol-level. Updated consumer imports in react, lit, and angular to use @a2ui/web_core/v0_9/basic_catalog. --- renderers/angular/src/v0_9/catalog/basic/column.component.ts | 2 +- renderers/angular/src/v0_9/catalog/basic/row.component.ts | 2 +- renderers/lit/src/v0_9/catalogs/minimal/components/Column.ts | 3 +-- renderers/lit/src/v0_9/catalogs/minimal/components/Row.ts | 3 +-- renderers/react/src/v0_9/catalog/basic/utils.ts | 2 +- renderers/react/src/v0_9/catalog/minimal/components/Column.tsx | 3 ++- renderers/react/src/v0_9/catalog/minimal/components/Row.tsx | 3 ++- renderers/web_core/src/v0_9/basic_catalog/index.ts | 1 + .../src/v0_9/{common => basic_catalog/styles}/layout.test.ts | 0 .../src/v0_9/{common => basic_catalog/styles}/layout.ts | 0 renderers/web_core/src/v0_9/index.ts | 1 - 11 files changed, 10 insertions(+), 10 deletions(-) rename renderers/web_core/src/v0_9/{common => basic_catalog/styles}/layout.test.ts (100%) rename renderers/web_core/src/v0_9/{common => basic_catalog/styles}/layout.ts (100%) diff --git a/renderers/angular/src/v0_9/catalog/basic/column.component.ts b/renderers/angular/src/v0_9/catalog/basic/column.component.ts index 5a1552c30..d4d5845bc 100644 --- a/renderers/angular/src/v0_9/catalog/basic/column.component.ts +++ b/renderers/angular/src/v0_9/catalog/basic/column.component.ts @@ -17,7 +17,7 @@ import { Component, input, computed, ChangeDetectionStrategy } from '@angular/core'; import { ComponentHostComponent } from '../../core/component-host.component'; import { BoundProperty } from '../../core/types'; -import { mapJustify, mapAlign } from '@a2ui/web_core/v0_9'; +import { mapJustify, mapAlign } from '@a2ui/web_core/v0_9/basic_catalog'; import { getNormalizedPath } from '../../core/utils'; /** diff --git a/renderers/angular/src/v0_9/catalog/basic/row.component.ts b/renderers/angular/src/v0_9/catalog/basic/row.component.ts index fe5c0796c..9c3a0f4f5 100644 --- a/renderers/angular/src/v0_9/catalog/basic/row.component.ts +++ b/renderers/angular/src/v0_9/catalog/basic/row.component.ts @@ -17,7 +17,7 @@ import { Component, input, computed, ChangeDetectionStrategy } from '@angular/core'; import { ComponentHostComponent } from '../../core/component-host.component'; import { BoundProperty } from '../../core/types'; -import { mapJustify, mapAlign } from '@a2ui/web_core/v0_9'; +import { mapJustify, mapAlign } from '@a2ui/web_core/v0_9/basic_catalog'; import { getNormalizedPath } from '../../core/utils'; /** diff --git a/renderers/lit/src/v0_9/catalogs/minimal/components/Column.ts b/renderers/lit/src/v0_9/catalogs/minimal/components/Column.ts index 5fe43c052..f5efbe5c5 100644 --- a/renderers/lit/src/v0_9/catalogs/minimal/components/Column.ts +++ b/renderers/lit/src/v0_9/catalogs/minimal/components/Column.ts @@ -18,8 +18,7 @@ import { html, nothing } from "lit"; import { customElement } from "lit/decorators.js"; import { map } from "lit/directives/map.js"; import { styleMap } from "lit/directives/style-map.js"; -import { ColumnApi } from "@a2ui/web_core/v0_9/basic_catalog"; -import { mapJustify, mapAlign } from "@a2ui/web_core/v0_9"; +import { ColumnApi, mapJustify, mapAlign } from "@a2ui/web_core/v0_9/basic_catalog"; import { A2uiLitElement, A2uiController } from "@a2ui/lit/v0_9"; @customElement("a2ui-column") diff --git a/renderers/lit/src/v0_9/catalogs/minimal/components/Row.ts b/renderers/lit/src/v0_9/catalogs/minimal/components/Row.ts index 470b56370..2fae59ea5 100644 --- a/renderers/lit/src/v0_9/catalogs/minimal/components/Row.ts +++ b/renderers/lit/src/v0_9/catalogs/minimal/components/Row.ts @@ -18,8 +18,7 @@ import { html, nothing } from "lit"; import { customElement } from "lit/decorators.js"; import { map } from "lit/directives/map.js"; import { styleMap } from "lit/directives/style-map.js"; -import { RowApi } from "@a2ui/web_core/v0_9/basic_catalog"; -import { mapJustify, mapAlign } from "@a2ui/web_core/v0_9"; +import { RowApi, mapJustify, mapAlign } from "@a2ui/web_core/v0_9/basic_catalog"; import { A2uiLitElement, A2uiController } from "@a2ui/lit/v0_9"; @customElement("a2ui-row") diff --git a/renderers/react/src/v0_9/catalog/basic/utils.ts b/renderers/react/src/v0_9/catalog/basic/utils.ts index 692e8b359..8ae254426 100644 --- a/renderers/react/src/v0_9/catalog/basic/utils.ts +++ b/renderers/react/src/v0_9/catalog/basic/utils.ts @@ -28,7 +28,7 @@ export const STANDARD_BORDER = '1px solid #ccc'; /** Standard border radius. */ export const STANDARD_RADIUS = '8px'; -export {mapJustify, mapAlign} from '@a2ui/web_core/v0_9'; +export {mapJustify, mapAlign} from '@a2ui/web_core/v0_9/basic_catalog'; export const getBaseLeafStyle = (): React.CSSProperties => ({ margin: LEAF_MARGIN, diff --git a/renderers/react/src/v0_9/catalog/minimal/components/Column.tsx b/renderers/react/src/v0_9/catalog/minimal/components/Column.tsx index f130af8b1..2036eab52 100644 --- a/renderers/react/src/v0_9/catalog/minimal/components/Column.tsx +++ b/renderers/react/src/v0_9/catalog/minimal/components/Column.tsx @@ -16,7 +16,8 @@ import {createReactComponent} from '../../../adapter'; import {z} from 'zod'; -import {CommonSchemas, mapJustify, mapAlign} from '@a2ui/web_core/v0_9'; +import {CommonSchemas} from '@a2ui/web_core/v0_9'; +import {mapJustify, mapAlign} from '@a2ui/web_core/v0_9/basic_catalog'; import {ChildList} from './ChildList'; export const ColumnSchema = z.object({ diff --git a/renderers/react/src/v0_9/catalog/minimal/components/Row.tsx b/renderers/react/src/v0_9/catalog/minimal/components/Row.tsx index 34e1ee765..91e80b54c 100644 --- a/renderers/react/src/v0_9/catalog/minimal/components/Row.tsx +++ b/renderers/react/src/v0_9/catalog/minimal/components/Row.tsx @@ -16,7 +16,8 @@ import {createReactComponent} from '../../../adapter'; import {z} from 'zod'; -import {CommonSchemas, mapJustify, mapAlign} from '@a2ui/web_core/v0_9'; +import {CommonSchemas} from '@a2ui/web_core/v0_9'; +import {mapJustify, mapAlign} from '@a2ui/web_core/v0_9/basic_catalog'; import {ChildList} from './ChildList'; export const RowSchema = z.object({ diff --git a/renderers/web_core/src/v0_9/basic_catalog/index.ts b/renderers/web_core/src/v0_9/basic_catalog/index.ts index 89572fdd2..05b9eeeba 100644 --- a/renderers/web_core/src/v0_9/basic_catalog/index.ts +++ b/renderers/web_core/src/v0_9/basic_catalog/index.ts @@ -18,3 +18,4 @@ export * from './expressions/expression_parser.js'; export * from './functions/basic_functions.js'; export * from './functions/basic_functions_api.js'; export * from './components/basic_components.js'; +export * from './styles/layout.js'; diff --git a/renderers/web_core/src/v0_9/common/layout.test.ts b/renderers/web_core/src/v0_9/basic_catalog/styles/layout.test.ts similarity index 100% rename from renderers/web_core/src/v0_9/common/layout.test.ts rename to renderers/web_core/src/v0_9/basic_catalog/styles/layout.test.ts diff --git a/renderers/web_core/src/v0_9/common/layout.ts b/renderers/web_core/src/v0_9/basic_catalog/styles/layout.ts similarity index 100% rename from renderers/web_core/src/v0_9/common/layout.ts rename to renderers/web_core/src/v0_9/basic_catalog/styles/layout.ts diff --git a/renderers/web_core/src/v0_9/index.ts b/renderers/web_core/src/v0_9/index.ts index b7a9bbdcd..51be3ccbb 100644 --- a/renderers/web_core/src/v0_9/index.ts +++ b/renderers/web_core/src/v0_9/index.ts @@ -24,7 +24,6 @@ export * from './catalog/function_invoker.js'; export * from './catalog/types.js'; export * from './common/events.js'; -export * from './common/layout.js'; export * from './processing/message-processor.js'; export * from './rendering/component-context.js'; export * from './rendering/data-context.js'; From 45224e8ceb919c2f0fd5c87aeafdd057d6373cd6 Mon Sep 17 00:00:00 2001 From: ppazosp Date: Tue, 21 Apr 2026 23:53:59 +0200 Subject: [PATCH 4/5] fix[web_core]: preserve pass-through and nullish semantics in layout mappers mapJustify/mapAlign previously coerced both undefined input and unknown enum values into hardcoded defaults (flex-start / stretch). This caused two silent regressions: - Undefined props produced a concrete CSS value, overriding inherited styles and CSS-variable theming (breaks the --a2ui-*-gap pattern introduced in #1166). - Unknown enum values were swallowed, masking future spec additions or malformed agent output from the browser's styling pipeline. Return undefined on nullish input so consumers leave the style unset. Pass unknown values through unchanged so they stay visible. Matches the pre-#1030 Angular behavior documented in row.component.spec.ts (align: 'baseline') and column.component.spec.ts ('missing justify and align' case). Tests updated in layout.test.ts; full suites still pass (web_core 273, angular 229, react 443, lit 70). --- .../v0_9/basic_catalog/styles/layout.test.ts | 25 +++++++---- .../src/v0_9/basic_catalog/styles/layout.ts | 45 +++++++++++-------- 2 files changed, 42 insertions(+), 28 deletions(-) diff --git a/renderers/web_core/src/v0_9/basic_catalog/styles/layout.test.ts b/renderers/web_core/src/v0_9/basic_catalog/styles/layout.test.ts index 7de8a233e..a74099561 100644 --- a/renderers/web_core/src/v0_9/basic_catalog/styles/layout.test.ts +++ b/renderers/web_core/src/v0_9/basic_catalog/styles/layout.test.ts @@ -14,8 +14,9 @@ * limitations under the License. */ -import assert from 'node:assert'; import {describe, it} from 'node:test'; +import * as assert from 'node:assert'; + import {mapAlign, mapJustify} from './layout.js'; describe('mapJustify', () => { @@ -40,11 +41,14 @@ describe('mapJustify', () => { it('maps stretch to stretch', () => { assert.strictEqual(mapJustify('stretch'), 'stretch'); }); - it('returns flex-start for undefined input', () => { - assert.strictEqual(mapJustify(undefined), 'flex-start'); + it('returns undefined for undefined input so consumers leave the style unset', () => { + assert.strictEqual(mapJustify(undefined), undefined); + }); + it('passes through unknown values unchanged', () => { + assert.strictEqual(mapJustify('unknown'), 'unknown'); }); - it('returns flex-start for unknown string input', () => { - assert.strictEqual(mapJustify('unknown'), 'flex-start'); + it('passes through empty string unchanged', () => { + assert.strictEqual(mapJustify(''), ''); }); }); @@ -61,10 +65,13 @@ describe('mapAlign', () => { it('maps stretch to stretch', () => { assert.strictEqual(mapAlign('stretch'), 'stretch'); }); - it('returns stretch for undefined input', () => { - assert.strictEqual(mapAlign(undefined), 'stretch'); + it('returns undefined for undefined input so consumers leave the style unset', () => { + assert.strictEqual(mapAlign(undefined), undefined); + }); + it('passes through unknown values unchanged (e.g. "baseline")', () => { + assert.strictEqual(mapAlign('baseline'), 'baseline'); }); - it('returns stretch for unknown string input', () => { - assert.strictEqual(mapAlign('unknown'), 'stretch'); + it('passes through empty string unchanged', () => { + assert.strictEqual(mapAlign(''), ''); }); }); diff --git a/renderers/web_core/src/v0_9/basic_catalog/styles/layout.ts b/renderers/web_core/src/v0_9/basic_catalog/styles/layout.ts index 90a52b6f0..b77404387 100644 --- a/renderers/web_core/src/v0_9/basic_catalog/styles/layout.ts +++ b/renderers/web_core/src/v0_9/basic_catalog/styles/layout.ts @@ -20,16 +20,13 @@ * Maps A2UI layout enum values (e.g., `spaceBetween`) to their corresponding * CSS values (e.g., `space-between`). These functions are shared across all * web renderers (React, Lit, Angular) to ensure consistent behavior. - */ - -/** - * Maps an A2UI justify enum value to its CSS `justify-content` equivalent. * - * @param value - An A2UI justify value such as `'start'`, `'center'`, - * `'spaceBetween'`, etc. - * @returns The corresponding CSS `justify-content` value. Defaults to - * `'flex-start'` for unrecognized or undefined input. + * Contract: nullish input returns `undefined` so consumers leave the CSS + * property unset (inherits from cascade / CSS variables). Unknown keys are + * passed through as-is so that future enum additions or spec mismatches + * remain visible to the browser rather than silently coerced to a default. */ + const justifyMap: Record = { center: 'center', end: 'flex-end', @@ -40,18 +37,19 @@ const justifyMap: Record = { stretch: 'stretch', }; -export function mapJustify(value?: string): string { - return (value && justifyMap[value]) || 'flex-start'; -} - /** - * Maps an A2UI align enum value to its CSS `align-items` equivalent. + * Maps an A2UI justify enum value to its CSS `justify-content` equivalent. * - * @param value - An A2UI align value such as `'start'`, `'center'`, `'end'`, - * or `'stretch'`. - * @returns The corresponding CSS `align-items` value. Defaults to `'stretch'` - * for unrecognized or undefined input. + * @param value - An A2UI justify value such as `'start'`, `'center'`, + * `'spaceBetween'`, etc. + * @returns The mapped CSS value, the raw input if the key is unknown, or + * `undefined` if the input is nullish. */ +export function mapJustify(value?: string): string | undefined { + if (value === undefined || value === null) return undefined; + return justifyMap[value] ?? value; +} + const alignMap: Record = { center: 'center', end: 'flex-end', @@ -59,6 +57,15 @@ const alignMap: Record = { stretch: 'stretch', }; -export function mapAlign(value?: string): string { - return (value && alignMap[value]) || 'stretch'; +/** + * Maps an A2UI align enum value to its CSS `align-items` equivalent. + * + * @param value - An A2UI align value such as `'start'`, `'center'`, `'end'`, + * or `'stretch'`. + * @returns The mapped CSS value, the raw input if the key is unknown, or + * `undefined` if the input is nullish. + */ +export function mapAlign(value?: string): string | undefined { + if (value === undefined || value === null) return undefined; + return alignMap[value] ?? value; } From 850e87ee2e22239cf92840740bacbb2cc5081bde Mon Sep 17 00:00:00 2001 From: ppazosp Date: Wed, 22 Apr 2026 00:05:33 +0200 Subject: [PATCH 5/5] fix[web_core]: harden layout maps per gemini review - Use Map instead of Record so prototype keys such as 'toString' no longer resolve to inherited functions. - Accept null alongside undefined in the parameter type; both return undefined so consumers leave the CSS property unset. - Add 'baseline' as an explicit entry in alignMap (previously worked via passthrough). Documents it as a supported alignment value. - Add tests for null input and the 'toString' prototype collision. --- .../v0_9/basic_catalog/styles/layout.test.ts | 19 +++++++- .../src/v0_9/basic_catalog/styles/layout.ts | 44 ++++++++++--------- 2 files changed, 41 insertions(+), 22 deletions(-) diff --git a/renderers/web_core/src/v0_9/basic_catalog/styles/layout.test.ts b/renderers/web_core/src/v0_9/basic_catalog/styles/layout.test.ts index a74099561..9b6179e01 100644 --- a/renderers/web_core/src/v0_9/basic_catalog/styles/layout.test.ts +++ b/renderers/web_core/src/v0_9/basic_catalog/styles/layout.test.ts @@ -44,9 +44,15 @@ describe('mapJustify', () => { it('returns undefined for undefined input so consumers leave the style unset', () => { assert.strictEqual(mapJustify(undefined), undefined); }); + it('returns undefined for null input', () => { + assert.strictEqual(mapJustify(null), undefined); + }); it('passes through unknown values unchanged', () => { assert.strictEqual(mapJustify('unknown'), 'unknown'); }); + it('does not resolve Object.prototype keys such as toString', () => { + assert.strictEqual(mapJustify('toString'), 'toString'); + }); it('passes through empty string unchanged', () => { assert.strictEqual(mapJustify(''), ''); }); @@ -65,11 +71,20 @@ describe('mapAlign', () => { it('maps stretch to stretch', () => { assert.strictEqual(mapAlign('stretch'), 'stretch'); }); + it('maps baseline to baseline', () => { + assert.strictEqual(mapAlign('baseline'), 'baseline'); + }); it('returns undefined for undefined input so consumers leave the style unset', () => { assert.strictEqual(mapAlign(undefined), undefined); }); - it('passes through unknown values unchanged (e.g. "baseline")', () => { - assert.strictEqual(mapAlign('baseline'), 'baseline'); + it('returns undefined for null input', () => { + assert.strictEqual(mapAlign(null), undefined); + }); + it('passes through unknown values unchanged', () => { + assert.strictEqual(mapAlign('unknown'), 'unknown'); + }); + it('does not resolve Object.prototype keys such as toString', () => { + assert.strictEqual(mapAlign('toString'), 'toString'); }); it('passes through empty string unchanged', () => { assert.strictEqual(mapAlign(''), ''); diff --git a/renderers/web_core/src/v0_9/basic_catalog/styles/layout.ts b/renderers/web_core/src/v0_9/basic_catalog/styles/layout.ts index b77404387..17ff75269 100644 --- a/renderers/web_core/src/v0_9/basic_catalog/styles/layout.ts +++ b/renderers/web_core/src/v0_9/basic_catalog/styles/layout.ts @@ -25,17 +25,20 @@ * property unset (inherits from cascade / CSS variables). Unknown keys are * passed through as-is so that future enum additions or spec mismatches * remain visible to the browser rather than silently coerced to a default. + * + * The maps use `Map` rather than plain objects to avoid prototype-chain + * lookups (e.g. `mapJustify('toString')` must not hit `Object.prototype`). */ -const justifyMap: Record = { - center: 'center', - end: 'flex-end', - spaceAround: 'space-around', - spaceBetween: 'space-between', - spaceEvenly: 'space-evenly', - start: 'flex-start', - stretch: 'stretch', -}; +const justifyMap = new Map([ + ['center', 'center'], + ['end', 'flex-end'], + ['spaceAround', 'space-around'], + ['spaceBetween', 'space-between'], + ['spaceEvenly', 'space-evenly'], + ['start', 'flex-start'], + ['stretch', 'stretch'], +]); /** * Maps an A2UI justify enum value to its CSS `justify-content` equivalent. @@ -45,27 +48,28 @@ const justifyMap: Record = { * @returns The mapped CSS value, the raw input if the key is unknown, or * `undefined` if the input is nullish. */ -export function mapJustify(value?: string): string | undefined { +export function mapJustify(value?: string | null): string | undefined { if (value === undefined || value === null) return undefined; - return justifyMap[value] ?? value; + return justifyMap.get(value) ?? value; } -const alignMap: Record = { - center: 'center', - end: 'flex-end', - start: 'flex-start', - stretch: 'stretch', -}; +const alignMap = new Map([ + ['baseline', 'baseline'], + ['center', 'center'], + ['end', 'flex-end'], + ['start', 'flex-start'], + ['stretch', 'stretch'], +]); /** * Maps an A2UI align enum value to its CSS `align-items` equivalent. * * @param value - An A2UI align value such as `'start'`, `'center'`, `'end'`, - * or `'stretch'`. + * `'stretch'`, or `'baseline'`. * @returns The mapped CSS value, the raw input if the key is unknown, or * `undefined` if the input is nullish. */ -export function mapAlign(value?: string): string | undefined { +export function mapAlign(value?: string | null): string | undefined { if (value === undefined || value === null) return undefined; - return alignMap[value] ?? value; + return alignMap.get(value) ?? value; }