;
+ }
+
+ return <>{renderComponent("root", "/")}>;
+};
diff --git a/renderers/react_prototype/src/adapter.test.tsx b/renderers/react_prototype/src/adapter.test.tsx
new file mode 100644
index 000000000..63ccaac33
--- /dev/null
+++ b/renderers/react_prototype/src/adapter.test.tsx
@@ -0,0 +1,128 @@
+/**
+ * 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 { describe, it, expect, vi } from 'vitest';
+import { render, screen, act } from '@testing-library/react';
+import { createReactComponent } from './adapter';
+import { ComponentContext, ComponentModel, SurfaceModel, Catalog, CommonSchemas } from '@a2ui/web_core/v0_9';
+import { z } from 'zod';
+
+const mockCatalog = new Catalog('test', [], []);
+
+describe('adapter', () => {
+ it('should render component with resolved props', () => {
+ const surface = new SurfaceModel('test-surface', mockCatalog);
+ const compModel = new ComponentModel('c1', 'TestComp', { text: 'Hello World', child: 'child1' });
+ surface.componentsModel.addComponent(compModel);
+
+ const context = new ComponentContext(surface, 'c1', '/');
+
+ const TestApiDef = {
+ name: 'TestComp',
+ schema: z.object({
+ text: CommonSchemas.DynamicString,
+ child: CommonSchemas.ComponentId
+ })
+ };
+
+ const TestComponent = createReactComponent(
+ TestApiDef,
+ ({ props, buildChild }) => {
+ return
;
+ }
+ );
+
+ const { unmount } = render( null} />);
+
+ expect(spyAddListener).toHaveBeenCalled();
+ // One listener added
+
+ unmount();
+
+ // We would need a way to check if removeListener was called, but checking that the binding logic doesn't crash on unmount is a good start.
+ // If listeners aren't cleaned up, subsequent updates might throw if component is destroyed.
+ });
+});
diff --git a/renderers/react_prototype/src/adapter.tsx b/renderers/react_prototype/src/adapter.tsx
new file mode 100644
index 000000000..913cd74d4
--- /dev/null
+++ b/renderers/react_prototype/src/adapter.tsx
@@ -0,0 +1,88 @@
+/**
+ * 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 React, { useRef, useSyncExternalStore, useCallback } from "react";
+import { z } from "zod";
+import { ComponentContext, GenericBinder } from "@a2ui/web_core/v0_9";
+import type { ComponentApi, ResolveA2uiProps } from "@a2ui/web_core/v0_9";
+
+export interface ReactComponentImplementation extends ComponentApi {
+ /** The framework-specific rendering wrapper. */
+ render: React.FC<{
+ context: ComponentContext;
+ buildChild: (id: string, basePath?: string) => React.ReactNode;
+ }>;
+}
+
+export type ReactA2uiComponentProps = {
+ props: T;
+ buildChild: (id: string, basePath?: string) => React.ReactNode;
+ context: ComponentContext;
+};
+
+// --- Component Factories ---
+
+/**
+ * Creates a React component implementation using the deep generic binder.
+ */
+export function createReactComponent(
+ api: { name: string; schema: Schema },
+ RenderComponent: React.FC>>>
+): ReactComponentImplementation {
+ type Props = ResolveA2uiProps>;
+
+ const ReactWrapper: React.FC<{ context: ComponentContext, buildChild: any }> = ({ context, buildChild }) => {
+ const bindingRef = useRef>(null);
+
+ if (!bindingRef.current) {
+ bindingRef.current = new GenericBinder(context, api.schema);
+ }
+ const binding = bindingRef.current;
+
+ const subscribe = useCallback((callback: () => void) => {
+ const sub = binding.subscribe(callback);
+ return () => sub.unsubscribe();
+ }, [binding]);
+
+ const getSnapshot = useCallback(() => binding.snapshot, [binding]);
+ const props = useSyncExternalStore(subscribe, getSnapshot);
+
+ return ;
+ };
+
+ return {
+ name: api.name,
+ schema: api.schema,
+ render: ReactWrapper
+ };
+}
+
+/**
+ * Creates a React component implementation that manages its own context bindings (no generic binder).
+ */
+export function createBinderlessComponent(
+ api: ComponentApi,
+ RenderComponent: React.FC<{
+ context: ComponentContext;
+ buildChild: (id: string, basePath?: string) => React.ReactNode;
+ }>
+): ReactComponentImplementation {
+ return {
+ name: api.name,
+ schema: api.schema,
+ render: RenderComponent
+ };
+}
diff --git a/renderers/react_prototype/src/basic_catalog/components/ReactAudioPlayer.tsx b/renderers/react_prototype/src/basic_catalog/components/ReactAudioPlayer.tsx
new file mode 100644
index 000000000..9c31af401
--- /dev/null
+++ b/renderers/react_prototype/src/basic_catalog/components/ReactAudioPlayer.tsx
@@ -0,0 +1,37 @@
+/**
+ * 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 React from "react";
+import { createReactComponent } from "../../adapter";
+import { AudioPlayerApi } from "@a2ui/web_core/v0_9/basic_catalog";
+import { getBaseLeafStyle } from "../utils";
+
+export const ReactAudioPlayer = createReactComponent(
+ AudioPlayerApi,
+ ({ props }) => {
+ const style: React.CSSProperties = {
+ ...getBaseLeafStyle(),
+ width: '100%'
+ };
+
+ return (
+
+ {props.description && {props.description}}
+
+
+ );
+ }
+);
diff --git a/renderers/react_prototype/src/basic_catalog/components/ReactButton.tsx b/renderers/react_prototype/src/basic_catalog/components/ReactButton.tsx
new file mode 100644
index 000000000..78f16f9f8
--- /dev/null
+++ b/renderers/react_prototype/src/basic_catalog/components/ReactButton.tsx
@@ -0,0 +1,49 @@
+/**
+ * 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 React from "react";
+import { createReactComponent } from "../../adapter";
+import { ButtonApi } from "@a2ui/web_core/v0_9/basic_catalog";
+import { LEAF_MARGIN } from "../utils";
+
+export const ReactButton = createReactComponent(
+ ButtonApi,
+ ({ props, buildChild }) => {
+ const style: React.CSSProperties = {
+ margin: LEAF_MARGIN,
+ padding: "8px 16px",
+ cursor: "pointer",
+ border: props.variant === "borderless" ? "none" : "1px solid #ccc",
+ backgroundColor: props.variant === "primary" ? "var(--a2ui-primary-color, #007bff)" : props.variant === "borderless" ? "transparent" : "#fff",
+ color: props.variant === "primary" ? "#fff" : "inherit",
+ borderRadius: "4px",
+ display: 'inline-flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ boxSizing: 'border-box'
+ };
+
+ return (
+
+ );
+ }
+);
diff --git a/renderers/react_prototype/src/basic_catalog/components/ReactCard.tsx b/renderers/react_prototype/src/basic_catalog/components/ReactCard.tsx
new file mode 100644
index 000000000..55e54edfe
--- /dev/null
+++ b/renderers/react_prototype/src/basic_catalog/components/ReactCard.tsx
@@ -0,0 +1,38 @@
+/**
+ * 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 React from "react";
+import { createReactComponent } from "../../adapter";
+import { CardApi } from "@a2ui/web_core/v0_9/basic_catalog";
+import { getBaseContainerStyle } from "../utils";
+
+export const ReactCard = createReactComponent(
+ CardApi,
+ ({ props, buildChild }) => {
+ const style: React.CSSProperties = {
+ ...getBaseContainerStyle(),
+ backgroundColor: '#fff',
+ boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
+ width: '100%'
+ };
+
+ return (
+
+ );
+ }
+);
diff --git a/renderers/react_prototype/src/basic_catalog/components/ReactCheckBox.tsx b/renderers/react_prototype/src/basic_catalog/components/ReactCheckBox.tsx
new file mode 100644
index 000000000..6e1606f65
--- /dev/null
+++ b/renderers/react_prototype/src/basic_catalog/components/ReactCheckBox.tsx
@@ -0,0 +1,49 @@
+/**
+ * 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 React from "react";
+import { createReactComponent } from "../../adapter";
+import { CheckBoxApi } from "@a2ui/web_core/v0_9/basic_catalog";
+import { LEAF_MARGIN } from "../utils";
+
+export const ReactCheckBox = createReactComponent(
+ CheckBoxApi,
+ ({ props }) => {
+ const onChange = (e: React.ChangeEvent) => {
+ props.setValue(e.target.checked);
+ };
+
+ const uniqueId = React.useId();
+
+ const hasError = props.validationErrors && props.validationErrors.length > 0;
+
+ return (
+
+
+
+ {props.label && }
+
+ {hasError && {props.validationErrors![0]}}
+
+ );
+ }
+);
diff --git a/renderers/react_prototype/src/basic_catalog/components/ReactChildList.tsx b/renderers/react_prototype/src/basic_catalog/components/ReactChildList.tsx
new file mode 100644
index 000000000..5da87d0f7
--- /dev/null
+++ b/renderers/react_prototype/src/basic_catalog/components/ReactChildList.tsx
@@ -0,0 +1,40 @@
+/**
+ * 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 React from "react";
+import { ComponentContext } from "@a2ui/web_core/v0_9";
+
+export const ReactChildList: React.FC<{
+ childList: any,
+ context: ComponentContext,
+ buildChild: (id: string, basePath?: string) => React.ReactNode
+}> = ({ childList, buildChild }) => {
+ if (Array.isArray(childList)) {
+ return <>{childList.map((item: any, i: number) => {
+ // The new binder outputs objects like { id: string, basePath: string } for arrays of structural nodes
+ if (item && typeof item === 'object' && item.id) {
+ return {buildChild(item.id, item.basePath)};
+ }
+ // Fallback for static string lists
+ if (typeof item === 'string') {
+ return {buildChild(item)};
+ }
+ return null;
+ })}>;
+ }
+
+ return null;
+};
diff --git a/renderers/react_prototype/src/basic_catalog/components/ReactChoicePicker.tsx b/renderers/react_prototype/src/basic_catalog/components/ReactChoicePicker.tsx
new file mode 100644
index 000000000..6e24347ac
--- /dev/null
+++ b/renderers/react_prototype/src/basic_catalog/components/ReactChoicePicker.tsx
@@ -0,0 +1,110 @@
+/**
+ * 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 React, { useState } from "react";
+import { createReactComponent } from "../../adapter";
+import { ChoicePickerApi } from "@a2ui/web_core/v0_9/basic_catalog";
+import { LEAF_MARGIN, STANDARD_BORDER, STANDARD_RADIUS } from "../utils";
+
+export const ReactChoicePicker = createReactComponent(
+ ChoicePickerApi,
+ ({ props, context }) => {
+ const [filter, setFilter] = useState('');
+
+ const values = Array.isArray(props.value) ? props.value : [];
+ const isMutuallyExclusive = props.variant === 'mutuallyExclusive';
+
+ const onToggle = (val: string) => {
+ if (isMutuallyExclusive) {
+ props.setValue([val]);
+ } else {
+ const newValues = values.includes(val)
+ ? values.filter((v: string) => v !== val)
+ : [...values, val];
+ props.setValue(newValues);
+ }
+ };
+
+ const options = (props.options || []).filter((opt: any) =>
+ !props.filterable || filter === '' || opt.label.toLowerCase().includes(filter.toLowerCase())
+ );
+
+ const containerStyle: React.CSSProperties = {
+ display: 'flex',
+ flexDirection: 'column',
+ gap: '8px',
+ margin: LEAF_MARGIN,
+ width: '100%'
+ };
+
+ const listStyle: React.CSSProperties = {
+ display: 'flex',
+ flexDirection: props.displayStyle === 'chips' ? 'row' : 'column',
+ flexWrap: props.displayStyle === 'chips' ? 'wrap' : 'nowrap',
+ gap: '8px'
+ };
+
+ return (
+
+ );
+ }
+);
diff --git a/renderers/react_prototype/src/basic_catalog/components/ReactColumn.tsx b/renderers/react_prototype/src/basic_catalog/components/ReactColumn.tsx
new file mode 100644
index 000000000..0f46bb68b
--- /dev/null
+++ b/renderers/react_prototype/src/basic_catalog/components/ReactColumn.tsx
@@ -0,0 +1,39 @@
+/**
+ * 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 { createReactComponent } from "../../adapter";
+import { ColumnApi } from "@a2ui/web_core/v0_9/basic_catalog";
+import { ReactChildList } from "./ReactChildList";
+import { mapJustify, mapAlign } from "../utils";
+
+export const ReactColumn = createReactComponent(
+ ColumnApi,
+ ({ props, buildChild, context }) => {
+ return (
+
+
+
+ );
+ }
+);
diff --git a/renderers/react_prototype/src/basic_catalog/components/ReactDateTimeInput.tsx b/renderers/react_prototype/src/basic_catalog/components/ReactDateTimeInput.tsx
new file mode 100644
index 000000000..124d9fc15
--- /dev/null
+++ b/renderers/react_prototype/src/basic_catalog/components/ReactDateTimeInput.tsx
@@ -0,0 +1,59 @@
+/**
+ * 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 React from "react";
+import { createReactComponent } from "../../adapter";
+import { DateTimeInputApi } from "@a2ui/web_core/v0_9/basic_catalog";
+import { LEAF_MARGIN, STANDARD_BORDER, STANDARD_RADIUS } from "../utils";
+
+export const ReactDateTimeInput = createReactComponent(
+ DateTimeInputApi,
+ ({ props }) => {
+ const onChange = (e: React.ChangeEvent) => {
+ props.setValue(e.target.value);
+ };
+
+ const uniqueId = React.useId();
+
+ // Map enableDate/enableTime to input type
+ let type = 'datetime-local';
+ if (props.enableDate && !props.enableTime) type = 'date';
+ if (!props.enableDate && props.enableTime) type = 'time';
+
+ const style: React.CSSProperties = {
+ padding: "8px",
+ width: "100%",
+ border: STANDARD_BORDER,
+ borderRadius: STANDARD_RADIUS,
+ boxSizing: "border-box"
+ };
+
+ return (
+
+ {props.label && }
+
+
+ );
+ }
+);
diff --git a/renderers/react_prototype/src/basic_catalog/components/ReactDivider.tsx b/renderers/react_prototype/src/basic_catalog/components/ReactDivider.tsx
new file mode 100644
index 000000000..9c82f43e9
--- /dev/null
+++ b/renderers/react_prototype/src/basic_catalog/components/ReactDivider.tsx
@@ -0,0 +1,42 @@
+/**
+ * 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 React from "react";
+import { createReactComponent } from "../../adapter";
+import { DividerApi } from "@a2ui/web_core/v0_9/basic_catalog";
+import { LEAF_MARGIN } from "../utils";
+
+export const ReactDivider = createReactComponent(
+ DividerApi,
+ ({ props }) => {
+ const isVertical = props.axis === 'vertical';
+ const style: React.CSSProperties = {
+ margin: LEAF_MARGIN,
+ border: 'none',
+ backgroundColor: '#ccc'
+ };
+
+ if (isVertical) {
+ style.width = '1px';
+ style.height = '100%';
+ } else {
+ style.width = '100%';
+ style.height = '1px';
+ }
+
+ return ;
+ }
+);
diff --git a/renderers/react_prototype/src/basic_catalog/components/ReactIcon.tsx b/renderers/react_prototype/src/basic_catalog/components/ReactIcon.tsx
new file mode 100644
index 000000000..94c6a0b31
--- /dev/null
+++ b/renderers/react_prototype/src/basic_catalog/components/ReactIcon.tsx
@@ -0,0 +1,42 @@
+/**
+ * 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 React from "react";
+import { createReactComponent } from "../../adapter";
+import { IconApi } from "@a2ui/web_core/v0_9/basic_catalog";
+import { getBaseLeafStyle } from "../utils";
+
+export const ReactIcon = createReactComponent(
+ IconApi,
+ ({ props }) => {
+ const iconName = typeof props.name === 'string' ? props.name : (props.name as any)?.path;
+ const style: React.CSSProperties = {
+ ...getBaseLeafStyle(),
+ fontSize: '24px',
+ width: '24px',
+ height: '24px',
+ display: 'inline-flex',
+ alignItems: 'center',
+ justifyContent: 'center'
+ };
+
+ return (
+
+ {iconName}
+
+ );
+ }
+);
diff --git a/renderers/react_prototype/src/basic_catalog/components/ReactImage.tsx b/renderers/react_prototype/src/basic_catalog/components/ReactImage.tsx
new file mode 100644
index 000000000..dc19d3570
--- /dev/null
+++ b/renderers/react_prototype/src/basic_catalog/components/ReactImage.tsx
@@ -0,0 +1,51 @@
+/**
+ * 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 React from "react";
+import { createReactComponent } from "../../adapter";
+import { ImageApi } from "@a2ui/web_core/v0_9/basic_catalog";
+import { getBaseLeafStyle } from "../utils";
+
+export const ReactImage = createReactComponent(
+ ImageApi,
+ ({ props }) => {
+ const style: React.CSSProperties = {
+ ...getBaseLeafStyle(),
+ objectFit: props.fit as any,
+ width: '100%',
+ height: 'auto',
+ display: 'block'
+ };
+
+ if (props.variant === 'icon') {
+ style.width = '24px';
+ style.height = '24px';
+ } else if (props.variant === 'avatar') {
+ style.width = '40px';
+ style.height = '40px';
+ style.borderRadius = '50%';
+ } else if (props.variant === 'smallFeature') {
+ style.maxWidth = '100px';
+ } else if (props.variant === 'largeFeature') {
+ style.maxHeight = '400px';
+ } else if (props.variant === 'header') {
+ style.height = '200px';
+ style.objectFit = 'cover';
+ }
+
+ return ;
+ }
+);
diff --git a/renderers/react_prototype/src/basic_catalog/components/ReactList.tsx b/renderers/react_prototype/src/basic_catalog/components/ReactList.tsx
new file mode 100644
index 000000000..768756d69
--- /dev/null
+++ b/renderers/react_prototype/src/basic_catalog/components/ReactList.tsx
@@ -0,0 +1,44 @@
+/**
+ * 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 React from "react";
+import { createReactComponent } from "../../adapter";
+import { ListApi } from "@a2ui/web_core/v0_9/basic_catalog";
+import { ReactChildList } from "./ReactChildList";
+import { mapAlign } from "../utils";
+
+export const ReactList = createReactComponent(
+ ListApi,
+ ({ props, buildChild, context }) => {
+ const isHorizontal = props.direction === 'horizontal';
+ const style: React.CSSProperties = {
+ display: "flex",
+ flexDirection: isHorizontal ? "row" : "column",
+ alignItems: mapAlign(props.align),
+ overflowX: isHorizontal ? "auto" : "hidden",
+ overflowY: isHorizontal ? "hidden" : "auto",
+ width: '100%',
+ margin: 0,
+ padding: 0
+ };
+
+ return (
+
+
+
+ );
+ }
+);
diff --git a/renderers/react_prototype/src/basic_catalog/components/ReactModal.tsx b/renderers/react_prototype/src/basic_catalog/components/ReactModal.tsx
new file mode 100644
index 000000000..eb7d4f588
--- /dev/null
+++ b/renderers/react_prototype/src/basic_catalog/components/ReactModal.tsx
@@ -0,0 +1,80 @@
+/**
+ * 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 { useState } from "react";
+import { createReactComponent } from "../../adapter";
+import { ModalApi } from "@a2ui/web_core/v0_9/basic_catalog";
+
+export const ReactModal = createReactComponent(
+ ModalApi,
+ ({ props, buildChild }) => {
+ const [isOpen, setIsOpen] = useState(false);
+
+ return (
+ <>
+
+ )}
+ >
+ );
+ }
+);
diff --git a/renderers/react_prototype/src/basic_catalog/components/ReactRow.tsx b/renderers/react_prototype/src/basic_catalog/components/ReactRow.tsx
new file mode 100644
index 000000000..3265e7d7d
--- /dev/null
+++ b/renderers/react_prototype/src/basic_catalog/components/ReactRow.tsx
@@ -0,0 +1,39 @@
+/**
+ * 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 { createReactComponent } from "../../adapter";
+import { RowApi } from "@a2ui/web_core/v0_9/basic_catalog";
+import { ReactChildList } from "./ReactChildList";
+import { mapJustify, mapAlign } from "../utils";
+
+export const ReactRow = createReactComponent(
+ RowApi,
+ ({ props, buildChild, context }) => {
+ return (
+
+
+
+ );
+ }
+);
diff --git a/renderers/react_prototype/src/basic_catalog/components/ReactSlider.tsx b/renderers/react_prototype/src/basic_catalog/components/ReactSlider.tsx
new file mode 100644
index 000000000..6c9f60d48
--- /dev/null
+++ b/renderers/react_prototype/src/basic_catalog/components/ReactSlider.tsx
@@ -0,0 +1,49 @@
+/**
+ * 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 React from "react";
+import { createReactComponent } from "../../adapter";
+import { SliderApi } from "@a2ui/web_core/v0_9/basic_catalog";
+import { LEAF_MARGIN } from "../utils";
+
+export const ReactSlider = createReactComponent(
+ SliderApi,
+ ({ props }) => {
+ const onChange = (e: React.ChangeEvent) => {
+ props.setValue(Number(e.target.value));
+ };
+
+ const uniqueId = React.useId();
+
+ return (
+
+
+ {props.label && }
+ {props.value}
+
+
+
+ );
+ }
+);
diff --git a/renderers/react_prototype/src/basic_catalog/components/ReactTabs.tsx b/renderers/react_prototype/src/basic_catalog/components/ReactTabs.tsx
new file mode 100644
index 000000000..103bac303
--- /dev/null
+++ b/renderers/react_prototype/src/basic_catalog/components/ReactTabs.tsx
@@ -0,0 +1,57 @@
+/**
+ * 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 { useState } from "react";
+import { createReactComponent } from "../../adapter";
+import { TabsApi } from "@a2ui/web_core/v0_9/basic_catalog";
+import { LEAF_MARGIN } from "../utils";
+
+export const ReactTabs = createReactComponent(
+ TabsApi,
+ ({ props, buildChild }) => {
+ const [selectedIndex, setSelectedIndex] = useState(0);
+
+ const tabs = props.tabs || [];
+ const activeTab = tabs[selectedIndex];
+
+ return (
+
+ );
+ }
+);
diff --git a/renderers/react_prototype/src/basic_catalog/components/ReactText.tsx b/renderers/react_prototype/src/basic_catalog/components/ReactText.tsx
new file mode 100644
index 000000000..d1c9da424
--- /dev/null
+++ b/renderers/react_prototype/src/basic_catalog/components/ReactText.tsx
@@ -0,0 +1,42 @@
+/**
+ * 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 React from "react";
+import { createReactComponent } from "../../adapter";
+import { TextApi } from "@a2ui/web_core/v0_9/basic_catalog";
+import { getBaseLeafStyle } from "../utils";
+
+export const ReactText = createReactComponent(
+ TextApi,
+ ({ props }) => {
+ const text = props.text ?? "";
+ const style: React.CSSProperties = {
+ ...getBaseLeafStyle(),
+ display: 'inline-block'
+ };
+
+ switch (props.variant) {
+ case "h1": return
{text}
;
+ case "h2": return
{text}
;
+ case "h3": return
{text}
;
+ case "h4": return
{text}
;
+ case "h5": return
{text}
;
+ case "caption": return {text};
+ case "body":
+ default: return {text};
+ }
+ }
+);
diff --git a/renderers/react_prototype/src/basic_catalog/components/ReactTextField.tsx b/renderers/react_prototype/src/basic_catalog/components/ReactTextField.tsx
new file mode 100644
index 000000000..40f5adcb0
--- /dev/null
+++ b/renderers/react_prototype/src/basic_catalog/components/ReactTextField.tsx
@@ -0,0 +1,59 @@
+/**
+ * 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 React from "react";
+import { createReactComponent } from "../../adapter";
+import { TextFieldApi } from "@a2ui/web_core/v0_9/basic_catalog";
+import { LEAF_MARGIN, STANDARD_BORDER, STANDARD_RADIUS } from "../utils";
+
+export const ReactTextField = createReactComponent(
+ TextFieldApi,
+ ({ props }) => {
+ const onChange = (e: React.ChangeEvent) => {
+ props.setValue(e.target.value);
+ };
+
+ const isLong = props.variant === "longText";
+ const type = props.variant === "number" ? "number" : props.variant === "obscured" ? "password" : "text";
+
+ const style: React.CSSProperties = {
+ padding: "8px",
+ width: "100%",
+ border: STANDARD_BORDER,
+ borderRadius: STANDARD_RADIUS,
+ boxSizing: "border-box"
+ };
+
+ // Note: To have a unique id without passing context we can use a random or provided id,
+ // but the simplest is just relying on React's useId if we really need it.
+ // For now, we'll omit the `id` from the label connection since we removed context.
+ const uniqueId = React.useId();
+
+ const hasError = props.validationErrors && props.validationErrors.length > 0;
+
+ return (
+
+ );
+ }
+);
diff --git a/renderers/react_prototype/src/basic_catalog/components/ReactVideo.tsx b/renderers/react_prototype/src/basic_catalog/components/ReactVideo.tsx
new file mode 100644
index 000000000..629854072
--- /dev/null
+++ b/renderers/react_prototype/src/basic_catalog/components/ReactVideo.tsx
@@ -0,0 +1,35 @@
+/**
+ * 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 React from "react";
+import { createReactComponent } from "../../adapter";
+import { VideoApi } from "@a2ui/web_core/v0_9/basic_catalog";
+import { getBaseLeafStyle } from "../utils";
+
+export const ReactVideo = createReactComponent(
+ VideoApi,
+ ({ props }) => {
+ const style: React.CSSProperties = {
+ ...getBaseLeafStyle(),
+ width: '100%',
+ aspectRatio: '16/9'
+ };
+
+ return (
+
+ );
+ }
+);
diff --git a/renderers/react_prototype/src/basic_catalog/index.ts b/renderers/react_prototype/src/basic_catalog/index.ts
new file mode 100644
index 000000000..5f849768f
--- /dev/null
+++ b/renderers/react_prototype/src/basic_catalog/index.ts
@@ -0,0 +1,86 @@
+/**
+ * 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 { Catalog } from "@a2ui/web_core/v0_9";
+import { BASIC_FUNCTIONS } from "@a2ui/web_core/v0_9/basic_catalog";
+import type { ReactComponentImplementation } from "../adapter";
+
+import { ReactText } from "./components/ReactText";
+import { ReactImage } from "./components/ReactImage";
+import { ReactIcon } from "./components/ReactIcon";
+import { ReactVideo } from "./components/ReactVideo";
+import { ReactAudioPlayer } from "./components/ReactAudioPlayer";
+import { ReactRow } from "./components/ReactRow";
+import { ReactColumn } from "./components/ReactColumn";
+import { ReactList } from "./components/ReactList";
+import { ReactCard } from "./components/ReactCard";
+import { ReactTabs } from "./components/ReactTabs";
+import { ReactDivider } from "./components/ReactDivider";
+import { ReactModal } from "./components/ReactModal";
+import { ReactButton } from "./components/ReactButton";
+import { ReactTextField } from "./components/ReactTextField";
+import { ReactCheckBox } from "./components/ReactCheckBox";
+import { ReactChoicePicker } from "./components/ReactChoicePicker";
+import { ReactSlider } from "./components/ReactSlider";
+import { ReactDateTimeInput } from "./components/ReactDateTimeInput";
+
+const basicComponents: ReactComponentImplementation[] = [
+ ReactText,
+ ReactImage,
+ ReactIcon,
+ ReactVideo,
+ ReactAudioPlayer,
+ ReactRow,
+ ReactColumn,
+ ReactList,
+ ReactCard,
+ ReactTabs,
+ ReactDivider,
+ ReactModal,
+ ReactButton,
+ ReactTextField,
+ ReactCheckBox,
+ ReactChoicePicker,
+ ReactSlider,
+ ReactDateTimeInput,
+];
+
+export const basicCatalog = new Catalog(
+ "https://a2ui.org/specification/v0_9/basic_catalog.json",
+ basicComponents,
+ BASIC_FUNCTIONS
+);
+
+export {
+ ReactText,
+ ReactImage,
+ ReactIcon,
+ ReactVideo,
+ ReactAudioPlayer,
+ ReactRow,
+ ReactColumn,
+ ReactList,
+ ReactCard,
+ ReactTabs,
+ ReactDivider,
+ ReactModal,
+ ReactButton,
+ ReactTextField,
+ ReactCheckBox,
+ ReactChoicePicker,
+ ReactSlider,
+ ReactDateTimeInput,
+};
diff --git a/renderers/react_prototype/src/basic_catalog/utils.ts b/renderers/react_prototype/src/basic_catalog/utils.ts
new file mode 100644
index 000000000..586498f4f
--- /dev/null
+++ b/renderers/react_prototype/src/basic_catalog/utils.ts
@@ -0,0 +1,65 @@
+/**
+ * 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 React from 'react';
+
+/** Standard leaf margin from the implementation guide. */
+export const LEAF_MARGIN = '8px';
+
+/** Standard internal padding for visually bounded containers. */
+export const CONTAINER_PADDING = '16px';
+
+/** Standard border for cards and inputs. */
+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 const getBaseLeafStyle = (): React.CSSProperties => ({
+ margin: LEAF_MARGIN,
+ boxSizing: 'border-box'
+});
+
+export const getBaseContainerStyle = (): React.CSSProperties => ({
+ margin: LEAF_MARGIN,
+ padding: CONTAINER_PADDING,
+ border: STANDARD_BORDER,
+ borderRadius: STANDARD_RADIUS,
+ boxSizing: 'border-box'
+});
diff --git a/renderers/react_prototype/src/catalog.ts b/renderers/react_prototype/src/catalog.ts
new file mode 100644
index 000000000..45e50f028
--- /dev/null
+++ b/renderers/react_prototype/src/catalog.ts
@@ -0,0 +1,55 @@
+/**
+ * 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 { Catalog, createFunctionImplementation } from "@a2ui/web_core/v0_9";
+import { ReactText } from "./components/ReactText";
+import { ReactButton } from "./components/ReactButton";
+import { ReactRow } from "./components/ReactRow";
+import { ReactColumn } from "./components/ReactColumn";
+import { ReactTextField } from "./components/ReactTextField";
+import type { ReactComponentImplementation } from "./adapter";
+import { z } from "zod";
+
+const minimalComponents: ReactComponentImplementation[] = [
+ ReactText,
+ ReactButton,
+ ReactRow,
+ ReactColumn,
+ ReactTextField
+];
+
+export const minimalCatalog = new Catalog(
+ "https://a2ui.org/specification/v0_9/catalogs/minimal/minimal_catalog.json",
+ minimalComponents,
+ [
+ createFunctionImplementation(
+ {
+ name: "capitalize",
+ returnType: "string",
+ schema: z.object({
+ value: z.any()
+ })
+ },
+ (args) => {
+ const val = args.value;
+ if (typeof val === "string") {
+ return val.toUpperCase();
+ }
+ return val as string;
+ }
+ )
+ ]
+);
\ No newline at end of file
diff --git a/renderers/react_prototype/src/components.test.tsx b/renderers/react_prototype/src/components.test.tsx
new file mode 100644
index 000000000..45252b103
--- /dev/null
+++ b/renderers/react_prototype/src/components.test.tsx
@@ -0,0 +1,168 @@
+/**
+ * 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 { describe, it, expect, vi } from 'vitest';
+import { render, screen, fireEvent } from '@testing-library/react';
+import { ComponentContext, ComponentModel, SurfaceModel, Catalog } from '@a2ui/web_core/v0_9';
+
+import { ReactText } from './components/ReactText';
+import { ReactButton } from './components/ReactButton';
+import { ReactRow } from './components/ReactRow';
+import { ReactColumn } from './components/ReactColumn';
+import { ReactTextField } from './components/ReactTextField';
+
+const mockCatalog = new Catalog('test', [], []);
+
+function createContext(type: string, properties: any) {
+ const surface = new SurfaceModel('test-surface', mockCatalog);
+ const compModel = new ComponentModel('test-id', type, properties);
+ surface.componentsModel.addComponent(compModel);
+ return new ComponentContext(surface, 'test-id', '/');
+}
+
+describe('ReactText', () => {
+ it('renders text and variant correctly', () => {
+ const ctx = createContext('Text', { text: 'Hello', variant: 'h1' });
+ const { container } = render( null} />);
+ const h1 = container.querySelector('h1');
+ expect(h1).not.toBeNull();
+ expect(h1?.textContent).toBe('Hello');
+ });
+
+ it('renders default variant', () => {
+ const ctx = createContext('Text', { text: 'Hello' });
+ const { container } = render( null} />);
+ const span = container.querySelector('span');
+ expect(span).not.toBeNull();
+ expect(span?.textContent).toBe('Hello');
+ });
+});
+
+describe('ReactButton', () => {
+ it('renders and dispatches action', () => {
+ const ctx = createContext('Button', {
+ child: 'btn-child',
+ variant: 'primary',
+ action: { event: { name: 'test_action' } }
+ });
+
+ const spy = vi.spyOn(ctx, 'dispatchAction').mockResolvedValue();
+
+ const buildChild = vi.fn().mockImplementation((id) => {id});
+
+ render();
+
+ const button = screen.getByRole('button');
+ expect(button).not.toBeNull();
+ expect(screen.getByTestId('child').textContent).toBe('btn-child');
+
+ // Check style for primary variant
+ expect(button.style.backgroundColor).toBe('rgb(0, 123, 255)'); // #007bff in rgb
+
+ fireEvent.click(button);
+ expect(spy).toHaveBeenCalledWith({ event: { name: 'test_action' } });
+ });
+});
+
+describe('ReactRow', () => {
+ it('renders children with correct flex styles', () => {
+ const ctx = createContext('Row', {
+ children: ['c1', 'c2'],
+ justify: 'spaceBetween',
+ align: 'center'
+ });
+
+ const buildChild = vi.fn().mockImplementation((id) =>
);
+
+ const { container } = render();
+ const colDiv = container.firstChild as HTMLElement;
+ expect(colDiv.style.display).toBe('flex');
+ expect(colDiv.style.flexDirection).toBe('column');
+ expect(colDiv.style.justifyContent).toBe('center');
+ expect(colDiv.style.alignItems).toBe('flex-start');
+
+ expect(screen.getByTestId('c1')).toBeDefined();
+ });
+});
+
+describe('ReactTextField', () => {
+ it('renders label and text input', () => {
+ const ctx = createContext('TextField', {
+ label: 'Username',
+ value: 'alice',
+ variant: 'shortText'
+ });
+
+ const { container } = render( null} />);
+ const label = container.querySelector('label');
+ expect(label?.textContent).toBe('Username');
+
+ const input = container.querySelector('input');
+ expect(input?.type).toBe('text');
+ expect(input?.value).toBe('alice');
+ });
+
+ it('renders textarea for longText', () => {
+ const ctx = createContext('TextField', {
+ label: 'Comments',
+ value: 'lots of text',
+ variant: 'longText'
+ });
+
+ const { container } = render( null} />);
+ const textarea = container.querySelector('textarea');
+ expect(textarea).not.toBeNull();
+ expect(textarea?.value).toBe('lots of text');
+ });
+
+ it('updates data model on change', () => {
+ const ctx = createContext('TextField', {
+ label: 'Username',
+ value: { path: '/user' }
+ });
+
+ const spySet = vi.spyOn(ctx.dataContext, 'set');
+
+ const { container } = render( null} />);
+ const input = container.querySelector('input');
+
+ fireEvent.change(input!, { target: { value: 'bob' } });
+
+ expect(spySet).toHaveBeenCalledWith('/user', 'bob');
+ });
+});
diff --git a/renderers/react_prototype/src/components/ReactButton.tsx b/renderers/react_prototype/src/components/ReactButton.tsx
new file mode 100644
index 000000000..eaafe88a2
--- /dev/null
+++ b/renderers/react_prototype/src/components/ReactButton.tsx
@@ -0,0 +1,51 @@
+/**
+ * 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 React from "react";
+import { createReactComponent } from "../adapter";
+import { z } from "zod";
+import { CommonSchemas } from "@a2ui/web_core/v0_9";
+
+export const ButtonSchema = z.object({
+ child: CommonSchemas.ComponentId,
+ action: CommonSchemas.Action,
+ variant: z.enum(["primary", "borderless"]).optional()
+});
+
+export const ButtonApiDef = {
+ name: "Button",
+ schema: ButtonSchema
+};
+
+export const ReactButton = createReactComponent(
+ ButtonApiDef,
+ ({ props, buildChild }) => {
+ const style: React.CSSProperties = {
+ padding: "8px 16px",
+ cursor: "pointer",
+ border: props.variant === "borderless" ? "none" : "1px solid #ccc",
+ backgroundColor: props.variant === "primary" ? "#007bff" : "transparent",
+ color: props.variant === "primary" ? "#fff" : "inherit",
+ borderRadius: "4px"
+ };
+
+ return (
+
+ );
+ }
+);
diff --git a/renderers/react_prototype/src/components/ReactChildList.tsx b/renderers/react_prototype/src/components/ReactChildList.tsx
new file mode 100644
index 000000000..5da87d0f7
--- /dev/null
+++ b/renderers/react_prototype/src/components/ReactChildList.tsx
@@ -0,0 +1,40 @@
+/**
+ * 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 React from "react";
+import { ComponentContext } from "@a2ui/web_core/v0_9";
+
+export const ReactChildList: React.FC<{
+ childList: any,
+ context: ComponentContext,
+ buildChild: (id: string, basePath?: string) => React.ReactNode
+}> = ({ childList, buildChild }) => {
+ if (Array.isArray(childList)) {
+ return <>{childList.map((item: any, i: number) => {
+ // The new binder outputs objects like { id: string, basePath: string } for arrays of structural nodes
+ if (item && typeof item === 'object' && item.id) {
+ return {buildChild(item.id, item.basePath)};
+ }
+ // Fallback for static string lists
+ if (typeof item === 'string') {
+ return {buildChild(item)};
+ }
+ return null;
+ })}>;
+ }
+
+ return null;
+};
diff --git a/renderers/react_prototype/src/components/ReactColumn.tsx b/renderers/react_prototype/src/components/ReactColumn.tsx
new file mode 100644
index 000000000..f492c0997
--- /dev/null
+++ b/renderers/react_prototype/src/components/ReactColumn.tsx
@@ -0,0 +1,65 @@
+/**
+ * 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 { createReactComponent } from "../adapter";
+import { z } from "zod";
+import { CommonSchemas } from "@a2ui/web_core/v0_9";
+import { ReactChildList } from "./ReactChildList";
+
+export const ColumnSchema = z.object({
+ children: CommonSchemas.ChildList,
+ justify: z.enum(["start", "center", "end", "spaceBetween", "spaceAround", "spaceEvenly", "stretch"]).optional(),
+ 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
+};
+
+export const ReactColumn = createReactComponent(
+ ColumnApiDef,
+ ({ props, buildChild, context }) => {
+ return (
+
+
+
+ );
+ }
+);
diff --git a/renderers/react_prototype/src/components/ReactRow.tsx b/renderers/react_prototype/src/components/ReactRow.tsx
new file mode 100644
index 000000000..40659f623
--- /dev/null
+++ b/renderers/react_prototype/src/components/ReactRow.tsx
@@ -0,0 +1,65 @@
+/**
+ * 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 { createReactComponent } from "../adapter";
+import { z } from "zod";
+import { CommonSchemas } from "@a2ui/web_core/v0_9";
+import { ReactChildList } from "./ReactChildList";
+
+export const RowSchema = z.object({
+ children: CommonSchemas.ChildList,
+ justify: z.enum(["center", "end", "spaceAround", "spaceBetween", "spaceEvenly", "start", "stretch"]).optional(),
+ 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
+};
+
+export const ReactRow = createReactComponent(
+ RowApiDef,
+ ({ props, buildChild, context }) => {
+ return (
+
+
+
+ );
+ }
+);
diff --git a/renderers/react_prototype/src/components/ReactText.tsx b/renderers/react_prototype/src/components/ReactText.tsx
new file mode 100644
index 000000000..857dce814
--- /dev/null
+++ b/renderers/react_prototype/src/components/ReactText.tsx
@@ -0,0 +1,46 @@
+/**
+ * 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 { createReactComponent } from "../adapter";
+import { z } from "zod";
+import { CommonSchemas } from "@a2ui/web_core/v0_9";
+
+export const TextSchema = z.object({
+ text: CommonSchemas.DynamicString,
+ variant: z.enum(["h1", "h2", "h3", "h4", "h5", "caption", "body"]).optional()
+});
+
+export const TextApiDef = {
+ name: "Text",
+ schema: TextSchema
+};
+
+export const ReactText = createReactComponent(
+ TextApiDef,
+ ({ props }) => {
+ const text = props.text ?? "";
+ switch (props.variant) {
+ case "h1": return
{text}
;
+ case "h2": return
{text}
;
+ case "h3": return
{text}
;
+ case "h4": return
{text}
;
+ case "h5": return
{text}
;
+ case "caption": return {text};
+ case "body":
+ default: return {text};
+ }
+ }
+);
diff --git a/renderers/react_prototype/src/components/ReactTextField.tsx b/renderers/react_prototype/src/components/ReactTextField.tsx
new file mode 100644
index 000000000..aacd25602
--- /dev/null
+++ b/renderers/react_prototype/src/components/ReactTextField.tsx
@@ -0,0 +1,70 @@
+/**
+ * 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 React from "react";
+import { createReactComponent } from "../adapter";
+import { z } from "zod";
+import { CommonSchemas } from "@a2ui/web_core/v0_9";
+
+export const TextFieldSchema = z.object({
+ label: CommonSchemas.DynamicString,
+ value: CommonSchemas.DynamicString,
+ variant: z.enum(["longText", "number", "shortText", "obscured"]).optional(),
+ validationRegexp: z.string().optional()
+});
+
+export const TextFieldApiDef = {
+ name: "TextField",
+ schema: TextFieldSchema
+};
+
+export const ReactTextField = createReactComponent(
+ TextFieldApiDef,
+ ({ props, context }) => {
+ const onChange = (e: React.ChangeEvent) => {
+ // In a reactive framework, we still update the DataModel directly for two-way binding.
+ // We look up the path from the un-resolved properties of the component model.
+ const valueProp = context.componentModel.properties.value;
+ if (valueProp && typeof valueProp === 'object' && valueProp.path) {
+ context.dataContext.set(valueProp.path, e.target.value);
+ }
+ };
+
+ const isLong = props.variant === "longText";
+ const type = props.variant === "number" ? "number" : props.variant === "obscured" ? "password" : "text";
+
+ const style: React.CSSProperties = {
+ padding: "8px",
+ width: "100%",
+ border: "1px solid #ccc",
+ borderRadius: "4px",
+ boxSizing: "border-box"
+ };
+
+ const id = `textfield-${context.componentModel.id}`;
+
+ return (
+
+ );
+}
diff --git a/samples/client/react/src/assets/react.svg b/samples/client/react/src/assets/react.svg
new file mode 100644
index 000000000..6c87de9bb
--- /dev/null
+++ b/samples/client/react/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/samples/client/react/src/index.css b/samples/client/react/src/index.css
new file mode 100644
index 000000000..f1e01cfe6
--- /dev/null
+++ b/samples/client/react/src/index.css
@@ -0,0 +1,84 @@
+/**
+ * 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.
+ */
+
+:root {
+ font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
+ line-height: 1.5;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+a {
+ font-weight: 500;
+ color: #646cff;
+ text-decoration: inherit;
+}
+a:hover {
+ color: #535bf2;
+}
+
+body {
+ margin: 0;
+ display: flex;
+ place-items: center;
+ min-width: 320px;
+ min-height: 100vh;
+}
+
+h1 {
+ font-size: 3.2em;
+ line-height: 1.1;
+}
+
+button {
+ border-radius: 8px;
+ border: 1px solid transparent;
+ padding: 0.6em 1.2em;
+ font-size: 1em;
+ font-weight: 500;
+ font-family: inherit;
+ background-color: #1a1a1a;
+ cursor: pointer;
+ transition: border-color 0.25s;
+}
+button:hover {
+ border-color: #646cff;
+}
+button:focus,
+button:focus-visible {
+ outline: 4px auto -webkit-focus-ring-color;
+}
+
+@media (prefers-color-scheme: light) {
+ :root {
+ color: #213547;
+ background-color: #ffffff;
+ }
+ a:hover {
+ color: #747bff;
+ }
+ button {
+ background-color: #f9f9f9;
+ }
+}
diff --git a/samples/client/react/src/integration.test.tsx b/samples/client/react/src/integration.test.tsx
new file mode 100644
index 000000000..3ab35c7fc
--- /dev/null
+++ b/samples/client/react/src/integration.test.tsx
@@ -0,0 +1,84 @@
+/**
+ * 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 { describe, it, expect } from 'vitest';
+import { render, screen, act, fireEvent } from '@testing-library/react';
+import React from 'react';
+import { MessageProcessor } from '@a2ui/web_core/v0_9';
+import { A2uiSurface, minimalCatalog } from '@a2ui/react_prototype';
+
+import ex1 from "../../../../specification/v0_9/json/catalogs/minimal/examples/1_simple_text.json";
+import ex2 from "../../../../specification/v0_9/json/catalogs/minimal/examples/2_row_layout.json";
+import ex4 from "../../../../specification/v0_9/json/catalogs/minimal/examples/4_login_form.json";
+
+describe('Gallery Integration Tests', () => {
+ it('renders Simple Text -> "Hello Minimal Catalog"', async () => {
+ const processor = new MessageProcessor([minimalCatalog as any], async () => {});
+ processor.processMessages(ex1.messages as any[]);
+
+ const surface = processor.model.getSurface("example_1");
+ expect(surface).toBeDefined();
+
+ render(
+
+
+
+ );
+
+ expect(screen.getByText('Hello, Minimal Catalog!')).toBeInTheDocument();
+ });
+
+ it('renders Row layout -> content visibility', async () => {
+ const processor = new MessageProcessor([minimalCatalog as any], async () => {});
+ processor.processMessages(ex2.messages as any[]);
+
+ const surface = processor.model.getSurface("example_2");
+ expect(surface).toBeDefined();
+
+ render(
+
+
+
+ );
+
+ expect(screen.getByText('Left Content')).toBeInTheDocument();
+ expect(screen.getByText('Right Content')).toBeInTheDocument();
+ });
+
+ it('handles Login form -> input updates data model', async () => {
+ const processor = new MessageProcessor([minimalCatalog as any], async () => {});
+ processor.processMessages(ex4.messages as any[]);
+
+ const surface = processor.model.getSurface("example_4");
+ expect(surface).toBeDefined();
+
+ render(
+
+
+
+ );
+
+ const usernameInput = screen.getByLabelText('Username') as HTMLInputElement;
+ expect(usernameInput).toBeDefined();
+
+ await act(async () => {
+ fireEvent.change(usernameInput, { target: { value: 'alice' } });
+ });
+
+ expect(surface!.dataModel.get('/username')).toBe('alice');
+ });
+});
+
diff --git a/samples/client/react/src/main.tsx b/samples/client/react/src/main.tsx
new file mode 100644
index 000000000..4380b3cb3
--- /dev/null
+++ b/samples/client/react/src/main.tsx
@@ -0,0 +1,26 @@
+/**
+ * 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 { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import './index.css'
+import App from './App.tsx'
+
+createRoot(document.getElementById('root')!).render(
+
+
+ ,
+)
diff --git a/samples/client/react/src/setupTests.ts b/samples/client/react/src/setupTests.ts
new file mode 100644
index 000000000..5816a5872
--- /dev/null
+++ b/samples/client/react/src/setupTests.ts
@@ -0,0 +1,17 @@
+/**
+ * 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 '@testing-library/jest-dom';
diff --git a/samples/client/react/tsconfig.app.json b/samples/client/react/tsconfig.app.json
new file mode 100644
index 000000000..06163a807
--- /dev/null
+++ b/samples/client/react/tsconfig.app.json
@@ -0,0 +1,29 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "types": ["vite/client"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "resolveJsonModule": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["src"]
+}
diff --git a/samples/client/react/tsconfig.json b/samples/client/react/tsconfig.json
new file mode 100644
index 000000000..1ffef600d
--- /dev/null
+++ b/samples/client/react/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/samples/client/react/tsconfig.node.json b/samples/client/react/tsconfig.node.json
new file mode 100644
index 000000000..8a67f62f4
--- /dev/null
+++ b/samples/client/react/tsconfig.node.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "ES2023",
+ "lib": ["ES2023"],
+ "module": "ESNext",
+ "types": ["node"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/samples/client/react/vite.config.ts b/samples/client/react/vite.config.ts
new file mode 100644
index 000000000..7e627d1a9
--- /dev/null
+++ b/samples/client/react/vite.config.ts
@@ -0,0 +1,37 @@
+/**
+ * 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 { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+import { resolve } from 'path'
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [react()],
+ resolve: {
+ alias: {
+ 'react': resolve(__dirname, 'node_modules/react'),
+ 'react-dom': resolve(__dirname, 'node_modules/react-dom'),
+ '@a2ui/react_prototype': resolve(__dirname, '../../../renderers/react_prototype/src/index.ts')
+ }
+ },
+ test: {
+ environment: 'jsdom',
+ globals: true,
+ setupFiles: ['./src/setupTests.ts']
+ }
+} as any)
diff --git a/specification/v0_9/docs/basic_catalog_implementation_guide.md b/specification/v0_9/docs/basic_catalog_implementation_guide.md
new file mode 100644
index 000000000..cc1a4acab
--- /dev/null
+++ b/specification/v0_9/docs/basic_catalog_implementation_guide.md
@@ -0,0 +1,287 @@
+# A2UI Basic Catalog Implementation Guide
+
+This guide is designed for renderer and client developers implementing the A2UI Basic Catalog (v0.9). It details how to visually present and functionally implement each component and client-side function defined in the catalog.
+
+When building your framework-specific adapters (Layer 3) over the generic A2UI bindings, refer to this document for the expected visual behaviors, suggested layouts, and interaction patterns. This guide uses generic terminology applicable to Web, Mobile (iOS/Android), and Desktop platforms.
+
+---
+
+## 1. Components
+
+### Text
+Displays text content.
+
+**Rendering Guidelines:** Text should be rendered using a Markdown parser when possible. If markdown rendering is unavailable or fails, gracefully fallback to rendering the raw text using the framework's default text primitive (e.g., `` in HTML, `Text` in Compose/SwiftUI).
+**Property Mapping:**
+- `variant="h1"` through `h5"`: Apply heading styling. Suggested relative font sizes: `h1` (2.5x base), `h2` (2x base), `h3` (1.75x base), `h4` (1.5x base), `h5` (1.25x base).
+- `variant="caption"`: Render as smaller text, typically italicized or in a lighter/muted color. Suggested font size: 0.8x base.
+- `variant="body"` (default): Standard body text. Uses the base font size (e.g., 16dp/16px).
+
+### Image
+Displays an image from a URL.
+
+**Rendering Guidelines:** Ensure the component defaults to a flexible width so it fills its container.
+**Property Mapping:**
+- `fit`: Map the property to the platform's equivalent content scaling mode (e.g., CSS `object-fit`, iOS `contentMode`, Android `ScaleType`).
+- `variant="icon"`: Render very small and square (e.g., 24x24dp).
+- `variant="avatar"`: Render small and rounded/circular (e.g., 40x40dp, fully rounded corners).
+- `variant="smallFeature"`: Render as a small rectangle (e.g., 100x100dp).
+- `variant="mediumFeature"` (default): Render as a medium rectangle (e.g., 100% width up to 300dp, or 200x200dp).
+- `variant="largeFeature"`: Render as a large prominent image (e.g., 100% width, max height 400dp).
+- `variant="header"`: Render as a full-width banner image, usually at the top of a surface (e.g., 100% width, height 200dp, scaling mode set to cover/crop).
+
+### Icon
+Displays a standard system icon.
+
+**Rendering Guidelines:** Map the icon `name` to a system or bundled icon set (e.g., Material Symbols, SF Symbols). The string `name` from the data model (e.g., `accountCircle`) should be converted to the required format (like snake_case `account_circle`) if required by the icon engine. Suggested styling: 24dp size and inherit the current text color.
+
+### Video
+A video player.
+
+**Rendering Guidelines:** Render using a native video player component with user controls enabled. Ensure the video container spans the full width of the parent's container for responsiveness.
+
+### AudioPlayer
+An audio player.
+
+**Rendering Guidelines:** Render using a native audio player component with user controls enabled. Like video, its container should span the full width of its parent.
+
+### Row
+A horizontal layout container.
+
+**Rendering Guidelines:** Implemented using a horizontal layout container (e.g., CSS Flexbox row, Compose `Row`, SwiftUI `HStack`). Ensure it fills the available width.
+**Property Mapping:**
+- `justify`: Maps to main-axis alignment (e.g., `justify-content` in CSS, `horizontalArrangement` in Compose). Use equivalents for pushing items to edges (`spaceBetween`) or packing them together (`start`, `center`, `end`).
+- `align`: Maps to cross-axis alignment (e.g., `align-items` in CSS, `verticalAlignment` in Compose). Use equivalents for top (`start`), center, or bottom (`end`).
+
+### Column
+A vertical layout container.
+
+**Rendering Guidelines:** Implemented using a vertical layout container (e.g., CSS Flexbox column, Compose `Column`, SwiftUI `VStack`).
+**Property Mapping:**
+- `justify`: Maps to main-axis alignment on the vertical axis.
+- `align`: Maps to cross-axis alignment on the horizontal axis.
+
+### List
+A scrollable list of components.
+
+**Rendering Guidelines:** Children of a horizontal list should typically have a constrained max-width so they do not stretch indefinitely.
+**Property Mapping:**
+- `direction="vertical"` (default): Implement as a vertically scrollable view (e.g., CSS `overflow-y: auto`, Compose `LazyColumn`, SwiftUI `ScrollView` vertical).
+- `direction="horizontal"`: Implement as a horizontally scrollable view. Hide the scrollbar for a cleaner look if supported by the platform.
+
+### Card
+A container with card-like styling that visually groups its child.
+
+**Rendering Guidelines:** Applies a background color distinct from the main surface, rounded corners (e.g., 8dp or 12dp), a subtle shadow or elevation, and inner padding (e.g., 16dp). Note that the card accepts exactly **one** child. If the user wants multiple elements inside a card, they must provide a container (like `Column`) as the single child.
+
+### Tabs
+A set of tabs, each with a title and a corresponding child component.
+
+**Rendering Guidelines:** Render a horizontal row of interactive tab headers for the `titles`. Visually indicate the active tab (e.g., bold text, colored bottom border).
+**Behavior & State:** Maintain a local `selectedIndex` state (defaulting to 0). When a tab header is tapped, update `selectedIndex` and render *only* the `child` component that corresponds to that index.
+
+### Divider
+A dividing line to separate content.
+
+**Property Mapping:**
+- `axis="horizontal"` (default): Render a 1dp tall line spanning 100% width with a subtle border/outline color.
+- `axis="vertical"`: Render a 1dp wide line with a set height, spanning the height of the container.
+
+### Modal
+A dialog window.
+
+**Rendering Guidelines:**
+- **Desktop UIs**: Render as a centered popup or native dialog window over the main content, typically with a dimmed backdrop.
+- **Mobile UIs**: Render as a bottom sheet or full-screen dialog over the main content.
+- You must provide a mechanism to close the modal (e.g., an "X" button, clicking/tapping the backdrop overlay, or a swipe-to-dismiss gesture).
+
+**Behavior & State:** This component behaves differently than a standard container. It acts as a **Modal Entry Point**. When instantiated, the user only sees the `trigger` child component on the screen (which usually acts and looks like a Button). The modal logic intercepts interactions (taps/clicks) on the `trigger`. When the `trigger` is tapped, the modal opens and displays the `content` child component.
+
+### Button
+An interactive button that dispatches a protocol action.
+
+**Rendering Guidelines:** Render as a native interactive button component. It must render its `child` component inside the button (usually a `Text` or `Icon`).
+**Behavior & State:** When tapped, it dispatches the `action` back to the server, dynamically resolving the context variables at the moment of the interaction.
+**Property Mapping:**
+- `variant="default"`: Standard button with a subtle background and border.
+- `variant="primary"`: Prominent call-to-action button using the theme's `primaryColor` for its background, and contrasting text.
+- `variant="borderless"`: Button with no background or border, appearing like a clickable text link.
+
+### TextField
+A field for user text input.
+
+**Rendering Guidelines:** Render using the platform's native text input control.
+**Behavior & State:** Establishes **Two-Way Binding**. As the user types, immediately write the new string back to the local data model path bound to `value`.
+**Property Mapping:**
+- `variant="shortText"` (default): Standard single-line input field.
+- `variant="longText"`: Render as a multi-line text area.
+- `variant="number"`: Render as a numeric input field, typically showing a numeric keyboard on mobile.
+- `variant="obscured"`: Render as an obscured password/secure field.
+
+### CheckBox
+A toggleable control with a label.
+
+**Rendering Guidelines:** Render a native checkbox or toggle switch component alongside a text label.
+**Behavior & State:** Triggers two-way binding on the `value` path, setting it to boolean `true` or `false` when interacted with.
+
+### ChoicePicker
+A component for selecting one or more options from a list.
+
+**Rendering Guidelines:**
+- `displayStyle="checkbox"` (default): Render as a dropdown menu, picker wheel, or an expanding vertical list of selectable options. A dropdown wrapper is preferred to save space.
+- `displayStyle="chips"`: Render as a horizontal, wrapping row of selectable chips/pills. Selected chips should have a distinct background/border.
+- If `filterable` is true, render a text input above the list of options. As the user types, filter the visible options using a case-insensitive substring match on the option labels.
+
+**Behavior & State:** Binds to an array of strings in the data model representing the active selections. Toggle selections in the data model upon user interaction.
+
+### Slider
+A control for selecting a numeric value within a range.
+
+**Rendering Guidelines:** Render using the platform's native slider or seek bar component. Optionally display the current numeric value next to the slider track.
+**Behavior & State:** Set `min` and `max` limits. Perform two-way binding, updating the numeric `value` path as the user drags the slider.
+
+### DateTimeInput
+An input for date and/or time.
+
+**Rendering Guidelines:** Render using native date and time picker controls.
+- If `enableDate` and `enableTime` are both true, show both date and time selection UI.
+- If only `enableDate` is true, show only a date picker.
+- If only `enableTime` is true, show only a time picker.
+
+**Behavior & State:** The component must convert the platform's native date/time format into a standard ISO 8601 string before writing it to the A2UI data model, and correctly parse ISO 8601 strings coming from the model into the input field.
+
+---
+
+## 2. Client-Side Functions
+
+Functions provide client-side logic for validation, interpolation, and operations. As defined in the Architecture Guide, the reactivity of function arguments is generally handled by the Core Data Layer (specifically the Binder/Context layer).
+
+When a function is called, the system resolves its arguments. If an argument is a static value, it is passed directly. If it is a dynamic binding, the Context layer handles the subscription. For most standard functions, the `execute` implementation simply receives a dictionary of static `args` and returns a static value. The Context layer wraps this execution in a reactive stream (e.g., a `computed` signal) so that the function re-runs whenever any of its dynamic arguments change.
+
+However, complex functions like `formatString` must manually interact with the Context to parse and subscribe to nested dynamic dependencies.
+
+### `formatString`
+**Description:** The core interpolation engine. Parses the `args.value` string for `${expression}` blocks, combining literal strings, data paths, and other client-side function results.
+
+**Architecture & Logic:**
+Because `formatString` contains dynamic expressions embedded *within* a string literal, the Context layer cannot pre-resolve them. The implementation must parse the string and manually create a reactive output.
+
+1. **Parser/Scanner:** Implement a parser that scans the input string (`args.value`) for `${...}` blocks. It must properly handle escaped markers (`\${`) which resolve to a literal `${`.
+2. **Expression Evaluation:** Inside the interpolation block, the parser must differentiate between:
+ - **Literals:** Quoted strings (`'...'` or `"..."`), numbers, and keywords (`true`, `false`, `null`).
+ - **Data Paths:** Identifiers starting with a slash (`/absolute/path`) or relative identifiers (`relative/path`).
+ - **Function Calls:** Identifiers followed by parentheses, e.g., `funcName(argName: value)`.
+3. **Context Resolution:** For every parsed `DataPath` or `FunctionCall` token, use the `DataContext` (e.g., `context.resolveSignal(token)`) to turn it into a reactive stream/signal.
+4. **Reactive Return:** The function MUST return a computed reactive stream (e.g., a `computed(() => ...)` signal). Inside this computed stream, unwrap all the resolved signals, convert them to strings, and concatenate them with the literal string parts.
+
+### `required`
+**Description:** Validates that a given value is present.
+
+**Logic:** Return `true` if `args.value` is strictly not `null`, not `undefined`, not an empty string `""`, and not an empty array `[]`. Otherwise, return `false`.
+
+### `regex`
+**Description:** Validates a value against a regular expression.
+
+**Logic:** Instantiate a regular expression using `args.pattern`. Test the `args.value` string against it. Return `true` if it matches, `false` otherwise.
+
+### `length`
+**Description:** Validates string length constraints.
+
+**Logic:** Ensure the length of the string `args.value` is `>= args.min` (if `min` is provided) and `<= args.max` (if `max` is provided).
+
+### `numeric`
+**Description:** Validates numeric range constraints.
+
+**Logic:** Parse `args.value` as a number. Ensure it is `>= args.min` (if `min` is provided) and `<= args.max` (if `max` is provided). Return `true` if valid, `false` if invalid or if it cannot be parsed as a number.
+
+### `email`
+**Description:** Validates an email address.
+
+**Logic:** Test `args.value` against a standard email regex pattern (e.g., `/^[^\s@]+@[^\s@]+\.[^\s@]+$/`).
+
+### `formatNumber`
+**Description:** Formats a numeric value.
+
+**Logic:** Use the platform's native locale formatting (e.g., `Intl.NumberFormat` on the web or `NumberFormatter` natively) on `args.value`.
+- If `args.decimals` is provided, force both the minimum and maximum fraction digits to that value.
+- Enable grouping (e.g., thousands separators) unless `args.grouping` is explicitly set to `false`.
+
+### `formatCurrency`
+**Description:** Formats a number as a currency string.
+
+**Logic:** Similar to `formatNumber`, but configured for currency style formatting. Apply the ISO 4217 currency code provided in `args.currency` (e.g., 'USD', 'EUR').
+
+### `formatDate`
+**Description:** Formats a timestamp into a date string.
+
+**Logic:** Parse `args.value` into a native Date/Time object. Interpret the Unicode TR35 `args.format` string (e.g., `yyyy-MM-dd`, `HH:mm`) and construct the formatted date string. You will likely need a platform-specific date formatting library to parse the TR35 pattern.
+
+### `pluralize`
+**Description:** Returns a localized pluralized string.
+
+**Logic:** Resolve the plural category for the numeric `args.value` based on the current locale (e.g., using `Intl.PluralRules` on the web). Map the resulting category (`zero`, `one`, `two`, `few`, `many`, `other`) to the corresponding string provided in the `args` object. If the specific category string is missing from `args`, fallback to `args.other`.
+
+### `openUrl`
+**Description:** Opens a URL.
+
+**Logic:** Open `args.url` using the native platform's URL handler (e.g., opening in the system browser or deep-linking to an app). This function returns `void` and is executed as a side-effect.
+
+### `and`
+**Description:** Logical AND operator.
+
+**Logic:** Iterate through the boolean array `args.values`. Return `true` only if all values are true. Short-circuit evaluation is encouraged.
+
+### `or`
+**Description:** Logical OR operator.
+
+**Logic:** Iterate through the boolean array `args.values`. Return `true` if at least one value is true. Short-circuit evaluation is encouraged.
+
+### `not`
+**Description:** Logical NOT operator.
+
+**Logic:** Return the strict boolean negation of `args.value`.
+
+---
+
+## 3. Layout Spacing: Margins and Padding
+
+A common challenge in dynamic UI frameworks is preventing "spacing multiplication," where nested containers (e.g., a `Text` inside a `Row` inside a `Column`) result in accumulated empty space that throws off the design.
+
+To achieve a clean, consistent default spacing where elements feel naturally separated without stacking empty space, implementers should follow a **Leaf-Margin Strategy**:
+
+1. **Invisible Containers have ZERO Spacing**: Structural, invisible layout containers (`Row`, `Column`, `List`) should have **no internal padding** and **no external margins**. They act purely as structural boundaries. This guarantees that wrapping an element in a `Row` or `Column` does not alter its spacing.
+2. **Leaf Components carry the Margin**: All non-container, visual "leaf" elements (`Text`, `Image`, `Icon`, `Video`, `AudioPlayer`, `Slider`, etc.) should have a uniform default **external margin** applied to them (e.g., `8dp` on all sides).
+3. **Visually Outlined Containers carry the Margin**: Containers and inputs that have a visible boundary (`Card`, `Button`, `TextField`, `CheckBox`, `ChoicePicker`) should also apply this same uniform default **external margin**.
+ - *Note:* These elements will naturally also need internal *padding* to keep their content away from their own visible borders, but this padding is localized and does not affect the external layout.
+
+**Why use Margins on Leaves?**
+Applying margins directly to the visual elements—rather than relying on padding or gap properties on the parent containers—ensures predictable spacing. For example, if you have `Row(Item1, Item2)`, using margins on the items guarantees that there is space to the left of `Item1`, space to the right of `Item2`, and space between them. Because the invisible containers themselves contribute zero extra spacing, you can deeply nest your structural rows and columns without the spacing unexpectedly multiplying.
+
+---
+
+## 4. Color, Contrast, and Nesting
+
+A common challenge in dynamically generated UI is ensuring proper contrast and visual hierarchy when components are nested. For example:
+- A `Text` or `Icon` nested inside a `primary` `Button` must change its color to contrast with the button's background.
+- A `Card` nested inside another `Card` should remain visually distinct.
+
+To keep the A2UI rendering layer simple and performant, **do not manually calculate or pass color properties down the A2UI component tree**. Instead, rely entirely on the native context and theme inheritance mechanisms provided by your target UI framework.
+
+### Text and Icon Contrast
+When an element defines a strong background color (like a `primary` `Button` using the theme's `primaryColor`), it must also define the expected text color for its content. It should propagate this expectation implicitly.
+
+- **Web (CSS):** The `Button` wrapper sets the standard CSS `color` property. Because `color` is inherited in CSS, any `Text` or `Icon` component rendered inside the button will automatically adapt.
+- **Compose (Android):** The button wrapper should use `CompositionLocalProvider(LocalContentColor provides ...)`. Any nested `Text` and `Icon` components will automatically pick up this color without needing it explicitly passed to their A2UI classes.
+- **SwiftUI (iOS):** Apply `.foregroundColor(...)` or `.environment(\.colorScheme, ...)` to the button wrapper.
+- **Flutter:** Use `DefaultTextStyle.merge()` and `IconTheme.merge()` within the button wrapper. If using standard Material buttons (like `ElevatedButton`), this is often handled for you automatically.
+
+*Rule of Thumb:* Leaf components like `Text` and `Icon` should **never** hardcode their colors unless explicitly instructed by a property. They must always inherit from their environment.
+
+### Nesting Containers (Cards)
+When a `Card` is nested within another `Card`, or placed on different background surfaces, it needs to remain distinct. Attempting to alternate surface colors based on depth adds significant complexity to the renderer.
+
+**Recommended Approach: Outlines and Transparent Surfaces**
+The simplest, most robust starting approach is to give `Card` components a **transparent background** and a **visible outline/border** (e.g., a 1dp outline matching the theme's outline/border color).
+- By using borders instead of opaque surface colors, nested cards will simply draw an inner boundary within the parent card.
+- This guarantees a clear visual hierarchy regardless of how deeply they are nested, and it requires zero context-passing or depth-tracking in your code.
+- If your design system requires opaque cards, consider using a framework-specific elevation system (e.g., standard Material elevation) which often handles shadow and surface tinting automatically, rather than building custom color-alternation logic into the A2UI adapters.
diff --git a/specification/v0_9/docs/renderer_guide.md b/specification/v0_9/docs/renderer_guide.md
index 21213e6c8..33413de25 100644
--- a/specification/v0_9/docs/renderer_guide.md
+++ b/specification/v0_9/docs/renderer_guide.md
@@ -28,9 +28,12 @@ In ecosystems dominated by a single UI framework (like iOS with SwiftUI), develo
## 1. The Core Data Layer (Framework Agnostic)
-The Data Layer is responsible for receiving the wire protocol (JSON messages), parsing them, and maintaining a long-lived, mutable state object. This layer follows the exact same design in all programming languages (with minor syntactical variations) and **does not require design work when porting to a new framework**.
+The A2UI client architecture follows a strict, unidirectional data flow that bridges language-agnostic data structures with native UI frameworks.
-> **Note on Language & Frameworks**: While the examples in this document are provided in TypeScript for clarity, the A2UI Data Layer is intended to be implemented in any language (e.g., Java, Python, Swift, Kotlin, Rust) and remain completely independent of any specific UI framework.
+1. **A2UI Messages** arrive from the server (JSON).
+2. The **`MessageProcessor`** parses these and updates the **`SurfaceModel`** (Agnostic State).
+3. The **`Surface`** (Framework Entry View) listens to the `SurfaceModel` and begins rendering.
+4. The `Surface` instantiates and renders individual **`ComponentImplementation`** nodes to build the UI tree.
It consists of three sub-components: the Processing Layer, the Models, and the Context Layer.
@@ -57,8 +60,6 @@ A2UI relies on a standard observer pattern to reactively update the UI when data
### Design Principles
-To ensure consistency and portability, the Data Layer implementation relies on standard patterns rather than framework-specific libraries.
-
#### 1. The "Add" Pattern for Composition
We strictly separate **construction** from **composition**. Parent containers do not act as factories for their children. This decoupling allows child classes to evolve their constructor signatures without breaking the parent. It also simplifies testing by allowing mock children to be injected easily.
@@ -80,7 +81,7 @@ The models must provide a mechanism for the rendering layer to observe changes.
5. **Consistency**: This pattern is used uniformly across the whole state model.
#### 3. Granular Reactivity
-The model is designed to support high-performance rendering through granular updates rather than full-surface refreshes.
+The model is designed to support high-performance rendering through granular updates.
* **Structure Changes**: The `SurfaceComponentsModel` notifies when items are added/removed.
* **Property Changes**: The `ComponentModel` notifies when its specific configuration changes.
* **Data Changes**: The `DataModel` notifies only subscribers to the specific path that changed.
@@ -145,6 +146,7 @@ class SurfaceModel {
dispatchAction(action: ActionEvent): Promise;
}
```
+
#### `SurfaceComponentsModel` & `ComponentModel`
Manages the raw JSON configuration of components in a flat map which includes one entry per component ID. This represents the raw Component data *before* ChildList templates are resolved, which can instantiate multiple instances of a single Component with the same ID.
@@ -161,38 +163,45 @@ class ComponentModel {
readonly id: string;
readonly type: string; // Component name (e.g. 'Button')
- get properties(): Record; // Current raw JSON configuration
+ get properties(): Record;
set properties(newProps: Record);
readonly onUpdated: EventSource; // Invoked when any property changes
}
```
+
#### `DataModel`
-A dedicated store for the surface's application data (the "Model" in MVVM).
+A dedicated store for application data.
```typescript
interface Subscription {
readonly value: T | undefined; // Latest evaluated value
- unsubscribe(): void; // Stop listening
+ unsubscribe(): void;
}
class DataModel {
get(path: string): any; // Resolve JSON Pointer to value
set(path: string, value: any): void; // Atomic update at path
subscribe(path: string, onChange: (v: T | undefined) => void): Subscription; // Reactive path monitoring
- dispose(): void; // Lifecycle cleanup
+ dispose(): void;
}
```
-#### JSON Pointer Implementation Rules
-To ensure parity across implementations, the `DataModel` must follow these rules:
+**JSON Pointer Implementation Rules**:
+1. **Auto-typing (Auto-vivification)**: When setting a value at a nested path (e.g., `/a/b/0/c`), create intermediate segments. If the next segment is numeric (`0`), initialize as an Array `[]`, otherwise an Object `{}`.
+2. **Notification Strategy (Bubble & Cascade)**: Notify exact matches, bubble up to all parent paths, and cascade down to all nested descendant paths.
+3. **Undefined Handling**: Setting an object key to `undefined` removes the key. Setting an array index to `undefined` preserves length but empties the index (sparse array).
-**1. Auto-typing (Auto-vivification)**
-When setting a value at a nested path (e.g., `/a/b/0/c`), if intermediate segments do not exist, the model must create them:
-* Look at the *next* segment in the path.
-* If the next segment is numeric (e.g., `0`, `12`), initialize the current segment as an **Array** `[]`.
-* Otherwise, initialize it as an **Object** `{}`.
-* **Error Case**: Throw an exception if an update attempts to traverse through a primitive value (e.g., setting `/a/b` when `/a` is already a string).
+**Type Coercion Standards**:
+| Input Type | Target Type | Result |
+| :------------------------- | :---------- | :---------------------------------------------------------------------- |
+| `String` ("true", "false") | `Boolean` | `true` or `false` (case-insensitive). Any other string maps to `false`. |
+| `Number` (non-zero) | `Boolean` | `true` |
+| `Number` (0) | `Boolean` | `false` |
+| `Any` | `String` | Locale-neutral string representation |
+| `null` / `undefined` | `String` | `""` (empty string) |
+| `null` / `undefined` | `Number` | `0` |
+| `String` (numeric) | `Number` | Parsed numeric value or `0` |
**2. Notification Strategy (The Bubble & Cascade)**
A change at a specific path must trigger notifications for related paths to ensure UI consistency:
@@ -235,12 +244,12 @@ class DataContext {
nested(relativePath: string): DataContext;
}
-class ComponentContext {
+class ComponentContext {
constructor(surface: SurfaceModel, componentId: string, basePath?: string);
- readonly componentModel: ComponentModel; // The instance configuration
- readonly dataContext: DataContext; // The instance's data scope
+ readonly componentModel: ComponentModel;
+ readonly dataContext: DataContext;
readonly surfaceComponents: SurfaceComponentsModel; // The escape hatch
- dispatchAction(action: any): Promise; // Propagate action to surface
+ dispatchAction(action: any): Promise;
}
```