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 03a64db5d..30c37b514 100644 --- a/renderers/angular/src/v0_9/catalog/basic/column.component.ts +++ b/renderers/angular/src/v0_9/catalog/basic/column.component.ts @@ -15,11 +15,10 @@ */ import { Component, computed, ChangeDetectionStrategy } from '@angular/core'; +import { mapJustify, mapAlign } from '@a2ui/web_core/v0_9/basic_catalog'; import { ComponentHostComponent } from '../../core/component-host.component'; - import { getNormalizedPath } from '../../core/utils'; import { BasicCatalogComponent } from './basic-catalog-component'; -import { JUSTIFY_MAP, ALIGN_MAP } from './utils'; /** * Angular implementation of the A2UI Column component (v0.9). @@ -63,14 +62,8 @@ import { JUSTIFY_MAP, ALIGN_MAP } from './utils'; 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 justify = computed(() => mapJustify(this.props()['justify']?.value())); + protected readonly align = computed(() => mapAlign(this.props()['align']?.value())); protected readonly 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 3044f2675..29edcd5f3 100644 --- a/renderers/angular/src/v0_9/catalog/basic/row.component.ts +++ b/renderers/angular/src/v0_9/catalog/basic/row.component.ts @@ -15,10 +15,10 @@ */ import { Component, computed, ChangeDetectionStrategy } from '@angular/core'; +import { mapJustify, mapAlign } from '@a2ui/web_core/v0_9/basic_catalog'; import { ComponentHostComponent } from '../../core/component-host.component'; import { getNormalizedPath } from '../../core/utils'; import { BasicCatalogComponent } from './basic-catalog-component'; -import { JUSTIFY_MAP, ALIGN_MAP } from './utils'; /** * Angular implementation of the A2UI Row component (v0.9). @@ -61,14 +61,8 @@ import { JUSTIFY_MAP, ALIGN_MAP } from './utils'; 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 justify = computed(() => mapJustify(this.props()['justify']?.value())); + protected readonly align = computed(() => mapAlign(this.props()['align']?.value())); protected readonly children = computed(() => { const raw = this.props()['children']?.value() || []; 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 a46513362..000000000 --- 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/react/src/v0_9/catalog/basic/utils.ts b/renderers/react/src/v0_9/catalog/basic/utils.ts index e2421afde..68949b69d 100644 --- a/renderers/react/src/v0_9/catalog/basic/utils.ts +++ b/renderers/react/src/v0_9/catalog/basic/utils.ts @@ -29,41 +29,7 @@ export const useBasicCatalogStyles = () => { }, []); }; -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/basic_catalog'; export const getBaseLeafStyle = (): React.CSSProperties => ({ boxSizing: 'border-box', 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 570513e6d..d448b1706 100644 --- a/renderers/web_core/src/v0_9/basic_catalog/index.ts +++ b/renderers/web_core/src/v0_9/basic_catalog/index.ts @@ -18,4 +18,5 @@ 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'; export {injectBasicCatalogStyles} from './styles/default.js'; 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 new file mode 100644 index 000000000..9b6179e01 --- /dev/null +++ b/renderers/web_core/src/v0_9/basic_catalog/styles/layout.test.ts @@ -0,0 +1,92 @@ +/* + * 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 {describe, it} from 'node:test'; +import * as assert from 'node:assert'; + +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 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(''), ''); + }); +}); + +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('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('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 new file mode 100644 index 000000000..17ff75269 --- /dev/null +++ b/renderers/web_core/src/v0_9/basic_catalog/styles/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. + * + * 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. + * + * The maps use `Map` rather than plain objects to avoid prototype-chain + * lookups (e.g. `mapJustify('toString')` must not hit `Object.prototype`). + */ + +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. + * + * @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 | null): string | undefined { + if (value === undefined || value === null) return undefined; + return justifyMap.get(value) ?? value; +} + +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'`, + * `'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 | null): string | undefined { + if (value === undefined || value === null) return undefined; + return alignMap.get(value) ?? value; +}