forked from google/A2UI
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathA2UIRenderer.tsx
More file actions
143 lines (130 loc) · 5.01 KB
/
A2UIRenderer.tsx
File metadata and controls
143 lines (130 loc) · 5.01 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
/**
* 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 {Suspense, useMemo, memo, type ReactNode} from 'react';
import {useA2UI} from '../hooks/useA2UI';
import {ComponentNode} from './ComponentNode';
import {type ComponentRegistry} from '../registry/ComponentRegistry';
import {cn} from '../lib/utils';
/** Default loading fallback - memoized to prevent recreation */
const DefaultLoadingFallback = memo(function DefaultLoadingFallback() {
return (
<div className="a2ui-loading" style={{padding: '16px', opacity: 0.5}}>
Loading...
</div>
);
});
export interface A2UIRendererProps {
/** The surface ID to render */
surfaceId: string;
/** Additional CSS classes for the surface container */
className?: string;
/** Fallback content when surface is not yet available */
fallback?: ReactNode;
/** Loading fallback for lazy-loaded components */
loadingFallback?: ReactNode;
/** Optional custom component registry */
registry?: ComponentRegistry;
}
/**
* A2UIRenderer - renders an A2UI surface.
*
* This is the main entry point for rendering A2UI content in your React app.
* It reads the surface state from the A2UI store and renders the component tree.
*
* Memoized to prevent unnecessary re-renders when props haven't changed.
*
* @example
* ```tsx
* function App() {
* return (
* <A2UIProvider onAction={handleAction}>
* <A2UIRenderer surfaceId="main" />
* </A2UIProvider>
* );
* }
* ```
*/
export const A2UIRenderer = memo(function A2UIRenderer({
surfaceId,
className,
fallback = null,
loadingFallback,
registry,
}: A2UIRendererProps) {
const {getSurface, version} = useA2UI();
// Get surface - this will re-render when version changes
const surface = getSurface(surfaceId);
console.log('A2UIRenderer: surfaceId=', surfaceId, 'surface=', surface, 'version=', version);
// Memoize surface styles to prevent object recreation
// Matches Lit renderer's transformation logic in surface.ts
const surfaceStyles = useMemo<React.CSSProperties>(() => {
if (!surface?.styles) return {};
const styles: React.CSSProperties & Record<string, string> = {};
for (const [key, value] of Object.entries(surface.styles)) {
switch (key) {
// Generate a color palette from the primary color.
// Values range from 0-100 where 0=black, 100=white, 50=primary color.
// Uses color-mix to create intermediate values.
case 'primaryColor': {
styles['--p-100'] = '#ffffff';
styles['--p-99'] = `color-mix(in srgb, ${value} 2%, white 98%)`;
styles['--p-98'] = `color-mix(in srgb, ${value} 4%, white 96%)`;
styles['--p-95'] = `color-mix(in srgb, ${value} 10%, white 90%)`;
styles['--p-90'] = `color-mix(in srgb, ${value} 20%, white 80%)`;
styles['--p-80'] = `color-mix(in srgb, ${value} 40%, white 60%)`;
styles['--p-70'] = `color-mix(in srgb, ${value} 60%, white 40%)`;
styles['--p-60'] = `color-mix(in srgb, ${value} 80%, white 20%)`;
styles['--p-50'] = String(value);
styles['--p-40'] = `color-mix(in srgb, ${value} 80%, black 20%)`;
styles['--p-35'] = `color-mix(in srgb, ${value} 70%, black 30%)`;
styles['--p-30'] = `color-mix(in srgb, ${value} 60%, black 40%)`;
styles['--p-25'] = `color-mix(in srgb, ${value} 50%, black 50%)`;
styles['--p-20'] = `color-mix(in srgb, ${value} 40%, black 60%)`;
styles['--p-15'] = `color-mix(in srgb, ${value} 30%, black 70%)`;
styles['--p-10'] = `color-mix(in srgb, ${value} 20%, black 80%)`;
styles['--p-5'] = `color-mix(in srgb, ${value} 10%, black 90%)`;
styles['--p-0'] = '#000000';
break;
}
case 'font': {
styles['--font-family'] = String(value);
styles['--font-family-flex'] = String(value);
break;
}
}
}
return styles;
}, [surface?.styles]);
// No surface yet
if (!surface || !surface.componentTree) {
return <>{fallback}</>;
}
// Use provided fallback or default memoized component
const actualLoadingFallback = loadingFallback ?? <DefaultLoadingFallback />;
return (
<div
className={cn('a2ui-surface', className)}
style={surfaceStyles}
data-surface-id={surfaceId}
data-version={version}
>
<Suspense fallback={actualLoadingFallback}>
<ComponentNode node={surface.componentTree} surfaceId={surfaceId} registry={registry} />
</Suspense>
</div>
);
});
export default A2UIRenderer;