Skip to content
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
6ab973e
Add gemignore
jacobsimionato Jan 29, 2026
89db879
Add design docs
jacobsimionato Feb 16, 2026
4f43a8e
Add core renderer APIs not trimmed
jacobsimionato Feb 16, 2026
3b9ce30
Merge branch 'main' into data-layer-1
jacobsimionato Feb 16, 2026
d2f663b
Make a scaled down version of web-core
jacobsimionato Feb 16, 2026
aa1196d
Fix client capabilities test
jacobsimionato Feb 16, 2026
ca866e3
Update renderers web core
jacobsimionato Feb 16, 2026
a01b828
Update data model
jacobsimionato Feb 18, 2026
53e9469
refactor(web_core): rename A2uiModel to SurfaceGroupModel in v0.9
jacobsimionato Feb 18, 2026
5e62b6d
refactor(web_core): use standard add pattern for SurfaceGroupModel an…
jacobsimionato Feb 18, 2026
b77455c
refactor(web_core): rename ComponentsModel to SurfaceComponentsModel
jacobsimionato Feb 18, 2026
a15804d
replace many docs with one doc
jacobsimionato Feb 18, 2026
d82673e
Fix doc
jacobsimionato Feb 19, 2026
7647e3d
Add manual edits
jacobsimionato Feb 19, 2026
310fc80
Improve framework renderer section
jacobsimionato Feb 19, 2026
951fa0b
Simplify design
jacobsimionato Feb 19, 2026
98b26c6
Merge branch 'main' into data-layer-1
jacobsimionato Feb 19, 2026
8a088f2
Enable scrict mode again
jacobsimionato Feb 22, 2026
b412047
fix: Address PR feedback
jacobsimionato Feb 22, 2026
14e2ca5
Fix message processor
jacobsimionato Feb 24, 2026
6e15d44
Fix docs
jacobsimionato Feb 24, 2026
e3b2f65
Address feedback
jacobsimionato Feb 26, 2026
68efefc
Improve tests
jacobsimionato Feb 26, 2026
bb1e362
Improve subscription API
jacobsimionato Feb 26, 2026
95ab135
fix wishy washy types
jacobsimionato Feb 26, 2026
dd34140
Fix path comment
jacobsimionato Feb 26, 2026
d70d5ee
Add todos for type checks
jacobsimionato Feb 26, 2026
b38c982
Rename to renderer guide
jacobsimionato Feb 26, 2026
b331957
Update message processor to remove getSurface
jacobsimionato Feb 26, 2026
5733cef
Fix minor issues
jacobsimionato Feb 26, 2026
248e574
Update renderer guide
jacobsimionato Feb 26, 2026
93bec2b
Updates including event emitter etc
jacobsimionato Feb 26, 2026
48edaed
reduce node version
jacobsimionato Feb 26, 2026
f5e3f2d
A few updates
jacobsimionato Feb 27, 2026
ed611f1
Update renderer_guide
jacobsimionato Feb 27, 2026
b13accd
Fix schema names
jacobsimionato Feb 27, 2026
a4fa67b
Some improvements
jacobsimionato Feb 27, 2026
144f703
Update surface components and renderer guide
jacobsimionato Feb 27, 2026
4839444
remvoe design alternatives
jacobsimionato Feb 27, 2026
6a5b9b6
Remove flutter
jacobsimionato Feb 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 26 additions & 4 deletions renderers/web_core/package-lock.json
Comment thread
ditman marked this conversation as resolved.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 12 additions & 2 deletions renderers/web_core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"prepack": "npm run build",
"build": "wireit",
"build:tsc": "wireit",
"copy-spec": "wireit"
"test": "wireit"
},
"wireit": {
"copy-spec": {
Expand Down Expand Up @@ -68,13 +68,23 @@
"!dist/**/*.min.js{,.map}"
],
"clean": "if-file-deleted"
},
"test": {
"command": "node --test dist/**/*.test.js",
"dependencies": [
"build"
]
}
},
"author": "Google",
"license": "Apache-2.0",
"devDependencies": {
"@types/node": "^24.10.1",
"@types/node": "^25.2.3",
Comment thread
ditman marked this conversation as resolved.
Outdated
"typescript": "^5.8.3",
"wireit": "^0.15.0-pre.2"
},
"dependencies": {
"zod": "^3.25.76",
"zod-to-json-schema": "^3.25.1"
}
}
107 changes: 107 additions & 0 deletions renderers/web_core/src/v0_9/catalog/schema_types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { z } from 'zod';

export const DataBinding = z.object({
path: z.string().describe('A JSON Pointer path to a value in the data model.')
});

export const FunctionCall = z.object({
call: z.string().describe('The name of the function to call.'),
args: z.record(z.any()).describe('Arguments passed to the function.'),
returnType: z.enum(['string', 'number', 'boolean', 'array', 'object', 'any', 'void']).default('boolean')
});

const LogicExpression: z.ZodType<any> = z.lazy(() => z.union([
z.object({ and: z.array(LogicExpression).min(1) }),
z.object({ or: z.array(LogicExpression).min(1) }),
z.object({ not: LogicExpression }),
z.intersection(FunctionCall, z.object({ returnType: z.literal('boolean').optional() })), // FunctionCall returning boolean
z.object({ true: z.literal(true) }),
z.object({ false: z.literal(false) })
]));

const DynamicString = z.union([
z.string(),
DataBinding,
// FunctionCall returning string (simplified schema for Zod, stricter in JSON Schema)
FunctionCall
]);

const DynamicNumber = z.union([
z.number(),
DataBinding,
FunctionCall
]);

const DynamicBoolean = z.union([
z.boolean(),
DataBinding,
LogicExpression
]);

const DynamicStringList = z.union([
z.array(z.string()),
DataBinding,
FunctionCall
]);

export const DynamicValue = z.union([
z.string(),
z.number(),
z.boolean(),
z.array(z.any()),
DataBinding,
FunctionCall
]);

export type DataBindingDef = z.infer<typeof DataBinding>;
export type FunctionCallDef = z.infer<typeof FunctionCall>;
export type DynamicValueDef = z.infer<typeof DynamicValue>;

const ComponentId = z.string().describe('The unique identifier for a component.');

const ChildList = z.union([
z.array(ComponentId).describe('A static list of child component IDs.'),
z.object({
componentId: ComponentId,
path: z.string().describe('The path to the list of component property objects in the data model.')
}).describe('A template for generating a dynamic list of children.')
]);

const Action = z.union([
z.object({
event: z.object({
name: z.string(),
context: z.record(DynamicValue).optional()
})
}).describe('Triggers a server-side event.'),
z.object({
functionCall: FunctionCall
}).describe('Executes a local client-side function.')
]);

const CheckRule = z.intersection(
LogicExpression,
z.object({
message: z.string().describe('The error message to display if the check fails.')
})
);

const Checkable = z.object({
checks: z.array(CheckRule).optional().describe('A list of checks to perform.')
});

export const CommonTypes = {
ComponentId,
ChildList,
DataBinding,
DynamicValue,
DynamicString,
DynamicNumber,
DynamicBoolean,
DynamicStringList,
FunctionCall,
LogicExpression,
CheckRule,
Checkable,
Action,
};
31 changes: 31 additions & 0 deletions renderers/web_core/src/v0_9/catalog/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@

import { z } from 'zod';

/**
* A definition of a UI component's API.
* This interface defines the contract for a component's capabilities and properties,
* independent of any specific rendering implementation.
*/
export interface ComponentApi {
/** The name of the component as it appears in the A2UI JSON (e.g., 'Button'). */
name: string;

/**
* The Zod schema describing the **custom properties** of this component.
*
* - MUST include catalog-specific common properties (e.g. 'weight').
* - MUST NOT include 'component', 'id', or 'accessibility' as those are
* handled by the framework/envelope.
*/
readonly schema: z.ZodType<any>;
}

export interface CatalogApi {
id: string;

/**
* A map of available components.
* This is readonly to encourage immutable extension patterns.
*/
readonly components: ReadonlyMap<string, ComponentApi>;
}
10 changes: 10 additions & 0 deletions renderers/web_core/src/v0_9/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@

export * from './state/data-model.js';
export * from './rendering/data-context.js';
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should each of these be a different library, so you can import from web_core/v0.9/state or web_core/v0.9/rendering, instead of from a big file? That would also alleviate issues with name collisions down the line?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sense. Maybe we can figure it out later though?

export * from './state/surface-model.js';
export * from './processing/message-processor.js';
export * from './catalog/types.js';
export * from './rendering/component-context.js';
export * from './state/surface-group-model.js';
export * from './state/surface-components-model.js';

137 changes: 137 additions & 0 deletions renderers/web_core/src/v0_9/processing/message-processor.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@

import assert from 'node:assert';
import { test, describe, it, beforeEach } from 'node:test';
import { MessageProcessor } from './message-processor.js';
import { CatalogApi } from '../catalog/types.js';

describe('MessageProcessor', () => {
let processor: MessageProcessor<CatalogApi>;
let testCatalog: CatalogApi;
let actions: any[] = [];

beforeEach(() => {
actions = [];
testCatalog = {
id: 'test-catalog',
components: new Map()
};
processor = new MessageProcessor<CatalogApi>([testCatalog], async (a) => { actions.push(a); });
});

it('creates surface', () => {
processor.processMessages([{
createSurface: {
surfaceId: 's1',
catalogId: 'test-catalog',
theme: {}
}
}]);
const surface = processor.getSurfaceModel('s1');
assert.ok(surface);
assert.strictEqual(surface.id, 's1');
});

it('updates components on correct surface', () => {
processor.processMessages([{
createSurface: { surfaceId: 's1', catalogId: 'test-catalog' }
}]);

processor.processMessages([{
updateComponents: {
surfaceId: 's1',
components: [{ id: 'root', component: 'Box' }]
}
}]);

const surface = processor.getSurfaceModel('s1');
assert.ok(surface?.componentsModel.get('root'));
});

it('updates existing components via message', () => {
processor.processMessages([{
createSurface: { surfaceId: 's1', catalogId: 'test-catalog' }
}]);

// Create
processor.processMessages([{
updateComponents: {
surfaceId: 's1',
components: [{ id: 'btn', component: 'Button', label: 'Initial' }]
}
}]);

const surface = processor.getSurfaceModel('s1');
const btn = surface?.componentsModel.get('btn');
assert.strictEqual(btn?.properties.label, 'Initial');

// Update
processor.processMessages([{
updateComponents: {
surfaceId: 's1',
components: [{ id: 'btn', component: 'Button', label: 'Updated' }]
}
}]);

assert.strictEqual(btn?.properties.label, 'Updated');
});

it('deletes surface', () => {
processor.processMessages([{
createSurface: { surfaceId: 's1', catalogId: 'test-catalog' }
}]);
assert.ok(processor.getSurfaceModel('s1'));

processor.processMessages([{
deleteSurface: { surfaceId: 's1' }
}]);
assert.strictEqual(processor.getSurfaceModel('s1'), undefined);
});

it('routes data model updates', () => {
processor.processMessages([{
createSurface: { surfaceId: 's1', catalogId: 'test-catalog' }
}]);

processor.processMessages([{
updateDataModel: {
surfaceId: 's1',
path: '/foo',
value: 'bar'
}
}]);

const surface = processor.getSurfaceModel('s1');
assert.strictEqual(surface?.dataModel.get('/foo'), 'bar');
});

it('notifies lifecycle listeners', () => {
let created: any = null;
let deletedId: string | null = null;

const unsubscribe = processor.addLifecycleListener({
onSurfaceCreated: (s) => created = s,
onSurfaceDeleted: (id) => deletedId = id
});

// Create
processor.processMessages([{
createSurface: { surfaceId: 's1', catalogId: 'test-catalog' }
}]);
assert.ok(created);
assert.strictEqual(created.id, 's1');

// Delete
processor.processMessages([{
deleteSurface: { surfaceId: 's1' }
}]);
assert.strictEqual(deletedId, 's1');

// Test Unsubscribe
created = null;
unsubscribe();
processor.processMessages([{
createSurface: { surfaceId: 's2', catalogId: 'test-catalog' }
}]);
assert.strictEqual(created, null);
});
});
Loading
Loading