rdesign/frontend/node_modules/@copilotkit/a2ui-renderer/dist/index.umd.js

3086 lines
103 KiB
JavaScript

(function(global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('react'), require('@a2ui/lit/0.8'), require('react/jsx-runtime'), require('clsx'), require('markdown-it'), require('@a2ui/lit')) :
typeof define === 'function' && define.amd ? define(['exports', 'react', '@a2ui/lit/0.8', 'react/jsx-runtime', 'clsx', 'markdown-it', '@a2ui/lit'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory((global.CopilotKitA2UIRenderer = {}), global.React,global.A2UILit,global.React,global.clsx,global.markdownit,global.A2UILit));
})(this, function(exports, react, _a2ui_lit_0_8, react_jsx_runtime, clsx, markdown_it, _a2ui_lit) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
//#region \0rolldown/runtime.js
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
key = keys[i];
if (!__hasOwnProp.call(to, key) && key !== except) {
__defProp(to, key, {
get: ((k) => from[k]).bind(null, key),
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
});
}
}
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
value: mod,
enumerable: true
}) : target, mod));
//#endregion
react = __toESM(react);
markdown_it = __toESM(markdown_it);
//#region src/react-renderer/theme/litTheme.ts
/**
* Default theme for A2UI React components.
*
* This theme uses the same CSS class conventions as the Lit renderer,
* ensuring visual consistency between React and Lit implementations.
*
* IMPORTANT: This theme must be kept in sync with the Lit renderer's internal
* styling. If Lit components change their class maps, this file must be updated
* to match. Ideally, Lit would export its default theme for direct import.
*
* Requires the structural styles to be injected:
* @example
* ```tsx
* import { A2UIProvider } from '@a2ui/react';
* import { injectStyles } from '@a2ui/react/styles';
*
* // Inject structural CSS at app startup
* injectStyles();
*
* function App() {
* return (
* <A2UIProvider>
* <A2UIRenderer surfaceId="main" />
* </A2UIProvider>
* );
* }
* ```
*/
const elementA = {
"typography-f-sf": true,
"typography-fs-n": true,
"typography-w-500": true,
"layout-as-n": true,
"layout-dis-iflx": true,
"layout-al-c": true,
"typography-td-none": true,
"color-c-p40": true
};
const elementAudio = { "layout-w-100": true };
const elementBody = {
"typography-f-s": true,
"typography-fs-n": true,
"typography-w-400": true,
"layout-mt-0": true,
"layout-mb-2": true,
"typography-sz-bm": true,
"color-c-n10": true
};
const elementButton = {
"typography-f-sf": true,
"typography-fs-n": true,
"typography-w-500": true,
"layout-pt-3": true,
"layout-pb-3": true,
"layout-pl-5": true,
"layout-pr-5": true,
"layout-mb-1": true,
"border-br-16": true,
"border-bw-0": true,
"border-c-n70": true,
"border-bs-s": true,
"color-bgc-s30": true,
"behavior-ho-80": true
};
const elementHeading = {
"typography-f-sf": true,
"typography-fs-n": true,
"typography-w-500": true,
"layout-mt-0": true,
"layout-mb-2": true
};
const elementIframe = { "behavior-sw-n": true };
const elementInput = {
"typography-f-sf": true,
"typography-fs-n": true,
"typography-w-400": true,
"layout-pl-4": true,
"layout-pr-4": true,
"layout-pt-2": true,
"layout-pb-2": true,
"border-br-6": true,
"border-bw-1": true,
"color-bc-s70": true,
"border-bs-s": true,
"layout-as-n": true,
"color-c-n10": true
};
const elementP = {
"typography-f-s": true,
"typography-fs-n": true,
"typography-w-400": true,
"layout-m-0": true,
"typography-sz-bm": true,
"layout-as-n": true,
"color-c-n10": true
};
const elementList = {
"typography-f-s": true,
"typography-fs-n": true,
"typography-w-400": true,
"layout-m-0": true,
"typography-sz-bm": true,
"layout-as-n": true,
"color-c-n10": true
};
const elementPre = {
"typography-f-c": true,
"typography-fs-n": true,
"typography-w-400": true,
"typography-sz-bm": true,
"typography-ws-p": true,
"layout-as-n": true
};
const elementTextarea = {
...elementInput,
"layout-r-none": true,
"layout-fs-c": true
};
const elementVideo = { "layout-el-cv": true };
const litTheme = {
additionalStyles: {
Button: {
background: "var(--primary, oklch(0.205 0 0))",
color: "var(--primary-foreground, oklch(0.985 0 0))",
"border-radius": "calc(var(--radius, 0.625rem) - 2px)",
cursor: "pointer",
width: "100%",
"--n-10": "var(--primary-foreground, oklch(0.985 0 0))",
"--n-35": "var(--primary-foreground, oklch(0.985 0 0))",
"--n-60": "var(--primary-foreground, oklch(0.985 0 0))"
},
Card: {
background: "var(--card, oklch(1 0 0))",
border: "1px solid var(--border, oklch(0.922 0 0))",
"border-radius": "var(--radius, 0.625rem)"
},
TextField: {
"background-color": "var(--background, oklch(1 0 0))",
"border-color": "var(--input, oklch(0.922 0 0))",
color: "var(--foreground, oklch(0.145 0 0))",
"border-radius": "var(--radius, 0.625rem)"
},
CheckBox: {
"--p-100": "var(--background, oklch(1 0 0))",
"--p-60": "var(--input, oklch(0.922 0 0))",
"--n-30": "var(--foreground, oklch(0.145 0 0))"
},
DateTimeInput: {
"background-color": "var(--background, oklch(1 0 0))",
"border-color": "var(--input, oklch(0.922 0 0))",
color: "var(--foreground, oklch(0.145 0 0))",
"border-radius": "var(--radius, 0.625rem)"
},
Modal: {
"--p-100": "var(--card, oklch(1 0 0))",
"--p-80": "var(--border, oklch(0.922 0 0))",
"border-radius": "var(--radius, 0.625rem)"
},
Text: { color: "var(--foreground, oklch(0.145 0 0))" }
},
components: {
AudioPlayer: {},
Divider: {},
Icon: {},
Image: {
all: {
"border-br-5": true,
"layout-el-cv": true,
"layout-w-100": true,
"layout-h-100": true
},
avatar: { "is-avatar": true },
header: {},
icon: {},
largeFeature: {},
mediumFeature: {},
smallFeature: {}
},
Text: {
all: {
"layout-w-100": true,
"layout-g-2": true
},
h1: {
"typography-f-sf": true,
"typography-v-r": true,
"typography-w-400": true,
"layout-m-0": true,
"layout-p-0": true,
"typography-sz-hs": true
},
h2: {
"typography-f-sf": true,
"typography-v-r": true,
"typography-w-400": true,
"layout-m-0": true,
"layout-p-0": true,
"typography-sz-tl": true
},
h3: {
"typography-f-sf": true,
"typography-v-r": true,
"typography-w-400": true,
"layout-m-0": true,
"layout-p-0": true,
"typography-sz-tl": true
},
h4: {
"typography-f-sf": true,
"typography-v-r": true,
"typography-w-400": true,
"layout-m-0": true,
"layout-p-0": true,
"typography-sz-bl": true
},
h5: {
"typography-f-sf": true,
"typography-v-r": true,
"typography-w-400": true,
"layout-m-0": true,
"layout-p-0": true,
"typography-sz-bm": true
},
body: {},
caption: {}
},
Video: {
"border-br-5": true,
"layout-el-cv": true
},
Card: {
"border-br-9": true,
"layout-p-4": true,
"color-bgc-n100": true
},
Column: { "layout-g-2": true },
List: {
"layout-g-4": true,
"layout-p-2": true
},
Modal: {
backdrop: { "color-bbgc-p60_20": true },
element: {
"border-br-2": true,
"color-bgc-p100": true,
"layout-p-4": true,
"border-bw-1": true,
"border-bs-s": true,
"color-bc-p80": true
}
},
Row: { "layout-g-4": true },
Tabs: {
container: {},
controls: {
all: {},
selected: {}
},
element: {}
},
Button: {
"layout-pt-2": true,
"layout-pb-2": true,
"layout-pl-3": true,
"layout-pr-3": true,
"border-bw-0": true,
"border-bs-s": true,
"typography-w-400": true
},
CheckBox: {
container: {
"layout-dsp-iflex": true,
"layout-al-c": true
},
element: {
"layout-m-0": true,
"layout-mr-2": true,
"layout-p-2": true,
"border-br-2": true,
"border-bw-1": true,
"border-bs-s": true,
"color-bgc-p100": true,
"color-bc-p60": true,
"color-c-n30": true
},
label: {
"typography-f-sf": true,
"typography-v-r": true,
"typography-w-400": true,
"layout-flx-1": true,
"typography-sz-ll": true
}
},
DateTimeInput: {
container: {
"typography-sz-bm": true,
"layout-w-100": true,
"layout-g-2": true,
"layout-dsp-flexvert": true
},
label: {
"color-c-p30": true,
"typography-sz-bm": true
},
element: {
"layout-pt-2": true,
"layout-pb-2": true,
"layout-pl-3": true,
"layout-pr-3": true,
"border-bw-1": true,
"border-bs-s": true
}
},
MultipleChoice: {
container: {},
label: {},
element: {}
},
Slider: {
container: {},
label: {},
element: {}
},
TextField: {
container: {
"typography-sz-bm": true,
"layout-w-100": true,
"layout-g-2": true,
"layout-dsp-flexvert": true
},
label: { "layout-flx-0": true },
element: {
"typography-sz-bm": true,
"layout-pt-2": true,
"layout-pb-2": true,
"layout-pl-3": true,
"layout-pr-3": true,
"border-bw-1": true,
"border-bs-s": true
}
}
},
elements: {
a: elementA,
audio: elementAudio,
body: elementBody,
button: elementButton,
h1: elementHeading,
h2: elementHeading,
h3: elementHeading,
h4: elementHeading,
h5: elementHeading,
iframe: elementIframe,
input: elementInput,
p: elementP,
pre: elementPre,
textarea: elementTextarea,
video: elementVideo
},
markdown: {
p: Object.keys(elementP),
h1: Object.keys(elementHeading),
h2: Object.keys(elementHeading),
h3: Object.keys(elementHeading),
h4: Object.keys(elementHeading),
h5: Object.keys(elementHeading),
ul: Object.keys(elementList),
ol: Object.keys(elementList),
li: Object.keys(elementList),
a: Object.keys(elementA),
strong: [],
em: ["typography-fs-n"]
}
};
/**
* Alias for litTheme - the default theme for A2UI React components.
* @see litTheme
*/
const defaultTheme = litTheme;
//#endregion
//#region src/react-renderer/theme/ThemeContext.tsx
/**
* React context for the A2UI theme.
*/
const ThemeContext = (0, react.createContext)(void 0);
/**
* Provider component that makes the A2UI theme available to descendant components.
*/
function ThemeProvider({ theme, children }) {
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ThemeContext.Provider, {
value: theme !== null && theme !== void 0 ? theme : defaultTheme,
children
});
}
/**
* Hook to access the current A2UI theme.
*
* @returns The current theme
* @throws If used outside of a ThemeProvider
*/
function useTheme() {
const theme = (0, react.useContext)(ThemeContext);
if (!theme) throw new Error("useTheme must be used within a ThemeProvider or A2UIProvider");
return theme;
}
/**
* Hook to optionally access the current A2UI theme.
*
* @returns The current theme, or undefined if not within a provider
*/
function useThemeOptional() {
return (0, react.useContext)(ThemeContext);
}
//#endregion
//#region src/react-renderer/core/A2UIProvider.tsx
/**
* Context for stable actions (never changes reference, prevents re-renders).
* Components that only need to dispatch actions or read data won't re-render.
*/
const A2UIActionsContext = (0, react.createContext)(null);
/**
* Context for reactive state (changes trigger re-renders).
* Only components that need to react to state changes subscribe to this.
*/
const A2UIStateContext = (0, react.createContext)(null);
/**
* Provider component that sets up the A2UI context for descendant components.
*
* This provider uses a two-context architecture for performance:
* - A2UIActionsContext: Stable actions that never change (no re-renders)
* - A2UIStateContext: Reactive state that triggers re-renders when needed
*
* @example
* ```tsx
* function App() {
* const handleAction = async (message) => {
* const response = await fetch('/api/a2ui', {
* method: 'POST',
* body: JSON.stringify(message)
* });
* const newMessages = await response.json();
* };
*
* return (
* <A2UIProvider onAction={handleAction}>
* <A2UIRenderer surfaceId="main" />
* </A2UIProvider>
* );
* }
* ```
*/
function A2UIProvider({ onAction, theme, children }) {
const processorRef = (0, react.useRef)(null);
if (!processorRef.current) processorRef.current = _a2ui_lit_0_8.Data.createSignalA2uiMessageProcessor();
const processor = processorRef.current;
const [version, setVersion] = (0, react.useState)(0);
const onActionRef = (0, react.useRef)(onAction !== null && onAction !== void 0 ? onAction : null);
onActionRef.current = onAction !== null && onAction !== void 0 ? onAction : null;
const actionsRef = (0, react.useRef)(null);
if (!actionsRef.current) actionsRef.current = {
processMessages: (messages) => {
processor.processMessages(messages);
setVersion((v) => v + 1);
},
setData: (node, path, value, surfaceId) => {
processor.setData(node, path, value, surfaceId);
setVersion((v) => v + 1);
},
dispatch: (message) => {
if (onActionRef.current) onActionRef.current(message);
},
clearSurfaces: () => {
processor.clearSurfaces();
setVersion((v) => v + 1);
},
getSurface: (surfaceId) => {
return processor.getSurfaces().get(surfaceId);
},
getSurfaces: () => {
return processor.getSurfaces();
},
getData: (node, path, surfaceId) => {
return processor.getData(node, path, surfaceId);
},
resolvePath: (path, dataContextPath) => {
return processor.resolvePath(path, dataContextPath);
}
};
const actions = actionsRef.current;
const stateValue = (0, react.useMemo)(() => ({ version }), [version]);
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(A2UIActionsContext.Provider, {
value: actions,
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(A2UIStateContext.Provider, {
value: stateValue,
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ThemeProvider, {
theme,
children
})
})
});
}
/**
* Hook to access stable A2UI actions (won't cause re-renders).
* Use this when you only need to dispatch actions or read data.
*
* @returns Stable actions object
* @throws If used outside of an A2UIProvider
*/
function useA2UIActions() {
const actions = (0, react.useContext)(A2UIActionsContext);
if (!actions) throw new Error("useA2UIActions must be used within an A2UIProvider");
return actions;
}
/**
* Hook to subscribe to A2UI state changes.
* Components using this will re-render when state changes.
*
* @returns Current version number
* @throws If used outside of an A2UIProvider
*/
function useA2UIState() {
const state = (0, react.useContext)(A2UIStateContext);
if (!state) throw new Error("useA2UIState must be used within an A2UIProvider");
return state;
}
/**
* Hook to access the full A2UI context (actions + state).
* Components using this will re-render when state changes.
*
* @returns The A2UI context value
* @throws If used outside of an A2UIProvider
*/
function useA2UIContext() {
const actions = useA2UIActions();
const state = useA2UIState();
return (0, react.useMemo)(() => ({
...actions,
processor: null,
version: state.version,
onAction: null
}), [actions, state.version]);
}
/**
* @deprecated Use useA2UIContext instead. This alias exists for backward compatibility only.
*/
const useA2UIStore = useA2UIContext;
/**
* @deprecated This selector pattern does not provide performance benefits with React Context.
* Components will re-render on any context change regardless of what you select.
* Use useA2UIContext() or useA2UI() directly instead.
*
* @param selector - Function to select a slice of state
* @returns The selected state
*/
function useA2UIStoreSelector(selector) {
return selector(useA2UIContext());
}
//#endregion
//#region src/react-renderer/hooks/useA2UI.ts
/**
* Main API hook for A2UI. Provides methods to process messages
* and access surface state.
*
* Note: This hook subscribes to state changes. Components using this
* will re-render when the A2UI state changes. For action-only usage
* (no re-renders), use useA2UIActions() instead.
*
* @returns Object with message processing and surface access methods
*
* @example
* ```tsx
* function ChatApp() {
* const { processMessages, getSurface } = useA2UI();
*
* useEffect(() => {
* const ws = new WebSocket('wss://agent.example.com');
* ws.onmessage = (event) => {
* const messages = JSON.parse(event.data);
* processMessages(messages);
* };
* return () => ws.close();
* }, [processMessages]);
*
* return <A2UIRenderer surfaceId="main" />;
* }
* ```
*/
function useA2UI() {
const actions = useA2UIActions();
const state = useA2UIState();
return {
processMessages: actions.processMessages,
getSurface: actions.getSurface,
getSurfaces: actions.getSurfaces,
clearSurfaces: actions.clearSurfaces,
version: state.version
};
}
//#endregion
//#region src/react-renderer/registry/ComponentRegistry.ts
/**
* Registry for A2UI components. Allows registration of custom components
* and supports lazy loading for code splitting.
*
* @example
* ```tsx
* const registry = new ComponentRegistry();
*
* // Register a component directly
* registry.register('Text', { component: Text });
*
* // Register with lazy loading
* registry.register('Modal', {
* component: () => import('./components/Modal'),
* lazy: true
* });
*
* // Use with A2UIRenderer
* <A2UIRenderer surfaceId="main" registry={registry} />
* ```
*/
var ComponentRegistry = class ComponentRegistry {
constructor() {
this.registry = /* @__PURE__ */ new Map();
this.lazyCache = /* @__PURE__ */ new Map();
}
/**
* Get the singleton instance of the registry.
* Use this for the default global registry.
*/
static getInstance() {
if (!ComponentRegistry._instance) ComponentRegistry._instance = new ComponentRegistry();
return ComponentRegistry._instance;
}
/**
* Reset the singleton instance.
* Useful for testing.
*/
static resetInstance() {
ComponentRegistry._instance = null;
}
/**
* Register a component type.
*
* @param type - The A2UI component type name (e.g., 'Text', 'Button')
* @param registration - The component registration
*/
register(type, registration) {
this.registry.set(type, registration);
}
/**
* Unregister a component type.
*
* @param type - The component type to unregister
*/
unregister(type) {
this.registry.delete(type);
this.lazyCache.delete(type);
}
/**
* Check if a component type is registered.
*
* @param type - The component type to check
* @returns True if the component is registered
*/
has(type) {
return this.registry.has(type);
}
/**
* Get a component by type. If the component is registered with lazy loading,
* returns a React.lazy wrapped component.
*
* @param type - The component type to get
* @returns The React component, or null if not found
*/
get(type) {
const registration = this.registry.get(type);
if (!registration) return null;
if (registration.lazy && typeof registration.component === "function") {
const cached = this.lazyCache.get(type);
if (cached) return cached;
const lazyComponent = (0, react.lazy)(registration.component);
this.lazyCache.set(type, lazyComponent);
return lazyComponent;
}
return registration.component;
}
/**
* Get all registered component types.
*
* @returns Array of registered type names
*/
getRegisteredTypes() {
return Array.from(this.registry.keys());
}
/**
* Clear all registrations.
*/
clear() {
this.registry.clear();
this.lazyCache.clear();
}
};
ComponentRegistry._instance = null;
//#endregion
//#region src/react-renderer/core/ComponentNode.tsx
/** Memoized loading fallback to avoid recreating on each render */
const LoadingFallback = (0, react.memo)(function LoadingFallback() {
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
className: "a2ui-loading",
style: {
padding: "8px",
opacity: .5
},
children: "Loading..."
});
});
/**
* ComponentNode - dynamically renders an A2UI component based on its type.
*
* Looks up the component in the registry and renders it with the appropriate props.
* Supports lazy-loaded components via React.Suspense.
*
* No wrapper div is rendered - the component's root div (e.g., .a2ui-image) is the
* direct flex child, exactly matching Lit's structure where the :host element IS
* the flex item. Each component handles --weight CSS variable on its root div.
*
* Memoized to prevent unnecessary re-renders when parent updates but node hasn't changed.
*/
const ComponentNode = (0, react.memo)(function ComponentNode({ node, surfaceId, registry }) {
const actualRegistry = registry !== null && registry !== void 0 ? registry : ComponentRegistry.getInstance();
const nodeType = node && typeof node === "object" && "type" in node ? node.type : null;
const Component = (0, react.useMemo)(() => nodeType ? actualRegistry.get(nodeType) : null, [actualRegistry, nodeType]);
if (!nodeType) {
if (node) console.warn("[A2UI] Invalid component node (not resolved?):", node);
return null;
}
if (!Component) {
console.warn(`[A2UI] Unknown component type: ${nodeType}`);
return null;
}
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react.Suspense, {
fallback: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoadingFallback, {}),
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Component, {
node,
surfaceId
})
});
});
//#endregion
//#region src/react-renderer/theme/utils.ts
/**
* Converts a theme class map (Record<string, boolean>) to a className string.
*
* @param classMap - An object where keys are class names and values are booleans
* @returns A space-separated string of class names where the value is true
*
* @example
* classMapToString({ 'a2ui-button': true, 'a2ui-button--primary': true, 'disabled': false })
* // Returns: 'a2ui-button a2ui-button--primary'
*/
function classMapToString(classMap) {
if (!classMap) return "";
return Object.entries(classMap).filter(([, enabled]) => enabled).map(([className]) => className).join(" ");
}
/**
* Converts an additional styles object (Record<string, string>) to a React style object.
*
* @param styles - An object with CSS property names as keys and values as strings
* @returns A React-compatible style object, or undefined if no styles
*
* @example
* stylesToObject({ 'background-color': 'red', 'font-size': '16px', '--custom-var': 'blue' })
* // Returns: { backgroundColor: 'red', fontSize: '16px', '--custom-var': 'blue' }
*/
function stylesToObject(styles) {
if (!styles || Object.keys(styles).length === 0) return void 0;
const result = {};
for (const [key, value] of Object.entries(styles)) if (key.startsWith("--")) result[key] = value;
else {
const camelKey = key.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
result[camelKey] = value;
}
return result;
}
//#endregion
//#region src/react-renderer/lib/utils.ts
/**
* Utility function to merge class names.
* Combines clsx for conditional classes.
*
* @param inputs - Class values to merge
* @returns Merged class name string
*
* @example
* cn('base-class', condition && 'conditional-class', { 'object-class': true })
*/
function cn(...inputs) {
return (0, clsx.clsx)(inputs);
}
/**
* Merges multiple class maps into a single class map.
* Uses Lit's Styles.merge() function directly for consistency.
*
* Lit's merge handles prefix conflicts: if you have 'layout-p-2' and 'layout-p-4',
* only the latter is kept (same prefix 'layout-p-' means they conflict).
*
* @param maps - Class maps to merge
* @returns A merged class map
*/
function mergeClassMaps(...maps) {
const validMaps = maps.filter((m) => m !== void 0);
if (validMaps.length === 0) return {};
return _a2ui_lit_0_8.Styles.merge(...validMaps);
}
//#endregion
//#region src/react-renderer/core/A2UIRenderer.tsx
/** Default loading fallback - memoized to prevent recreation */
const DefaultLoadingFallback = (0, react.memo)(function DefaultLoadingFallback() {
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
className: "a2ui-loading",
style: {
padding: "16px",
opacity: .5
},
children: "Loading..."
});
});
/**
* 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>
* );
* }
* ```
*/
const A2UIRenderer = (0, react.memo)(function A2UIRenderer({ surfaceId, className, fallback = null, loadingFallback, registry }) {
const { getSurface, version } = useA2UI();
const surface = getSurface(surfaceId);
const surfaceStyles = (0, react.useMemo)(() => {
if (!(surface === null || surface === void 0 ? void 0 : surface.styles)) return {};
const styles = {};
for (const [key, value] of Object.entries(surface.styles)) switch (key) {
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 === null || surface === void 0 ? void 0 : surface.styles]);
if (!surface || !surface.componentTree) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: fallback });
const actualLoadingFallback = loadingFallback !== null && loadingFallback !== void 0 ? loadingFallback : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DefaultLoadingFallback, {});
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
className: cn("a2ui-surface", className),
style: surfaceStyles,
"data-surface-id": surfaceId,
"data-version": version,
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react.Suspense, {
fallback: actualLoadingFallback,
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ComponentNode, {
node: surface.componentTree,
surfaceId,
registry
})
})
});
});
//#endregion
//#region src/react-renderer/hooks/useA2UIComponent.ts
/**
* Base hook for A2UI components. Provides data binding, theme access,
* and action dispatching.
*
* @param node - The component node from the A2UI message processor
* @param surfaceId - The surface ID this component belongs to
* @returns Object with theme, data binding helpers, and action dispatcher
*
* @example
* ```tsx
* function TextField({ node, surfaceId }: A2UIComponentProps<Types.TextFieldNode>) {
* const { theme, resolveString, setValue } = useA2UIComponent(node, surfaceId);
*
* const label = resolveString(node.properties.label);
* const value = resolveString(node.properties.text) ?? '';
*
* return (
* <div className={classMapToString(theme.components.TextField.container)}>
* <label>{label}</label>
* <input
* value={value}
* onChange={(e) => setValue(node.properties.text?.path!, e.target.value)}
* />
* </div>
* );
* }
* ```
*/
function useA2UIComponent(node, surfaceId) {
const actions = useA2UIActions();
const theme = useTheme();
const baseId = (0, react.useId)();
useA2UIState();
/**
* Resolve a StringValue to its actual string value.
* Checks literalString, literal, then path in that order.
* Note: This reads from data model via stable actions reference.
*/
const resolveString = (0, react.useCallback)((value) => {
if (!value) return null;
if (typeof value !== "object") return null;
if (value.literalString !== void 0) return value.literalString;
if (value.literal !== void 0) return String(value.literal);
if (value.path) {
const data = actions.getData(node, value.path, surfaceId);
return data !== null ? String(data) : null;
}
return null;
}, [
actions,
node,
surfaceId
]);
/**
* Resolve a NumberValue to its actual number value.
*/
const resolveNumber = (0, react.useCallback)((value) => {
if (!value) return null;
if (typeof value !== "object") return null;
if (value.literalNumber !== void 0) return value.literalNumber;
if (value.literal !== void 0) return Number(value.literal);
if (value.path) {
const data = actions.getData(node, value.path, surfaceId);
return data !== null ? Number(data) : null;
}
return null;
}, [
actions,
node,
surfaceId
]);
/**
* Resolve a BooleanValue to its actual boolean value.
*/
const resolveBoolean = (0, react.useCallback)((value) => {
if (!value) return null;
if (typeof value !== "object") return null;
if (value.literalBoolean !== void 0) return value.literalBoolean;
if (value.literal !== void 0) return Boolean(value.literal);
if (value.path) {
const data = actions.getData(node, value.path, surfaceId);
return data !== null ? Boolean(data) : null;
}
return null;
}, [
actions,
node,
surfaceId
]);
/**
* Set a value in the data model for two-way binding.
*/
const setValue = (0, react.useCallback)((path, value) => {
actions.setData(node, path, value, surfaceId);
}, [
actions,
node,
surfaceId
]);
/**
* Get a value from the data model.
*/
const getValue = (0, react.useCallback)((path) => {
return actions.getData(node, path, surfaceId);
}, [
actions,
node,
surfaceId
]);
/**
* Dispatch a user action to the server.
* Resolves all context bindings before dispatching.
*/
const sendAction = (0, react.useCallback)((action) => {
const actionContext = {};
if (action.context) {
for (const item of action.context) if (item.value.literalString !== void 0) actionContext[item.key] = item.value.literalString;
else if (item.value.literalNumber !== void 0) actionContext[item.key] = item.value.literalNumber;
else if (item.value.literalBoolean !== void 0) actionContext[item.key] = item.value.literalBoolean;
else if (item.value.path) {
const resolvedPath = actions.resolvePath(item.value.path, node.dataContextPath);
actionContext[item.key] = actions.getData(node, resolvedPath, surfaceId);
}
}
actions.dispatch({ userAction: {
name: action.name,
sourceComponentId: node.id,
surfaceId,
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
context: actionContext
} });
}, [
actions,
node,
surfaceId
]);
/**
* Generate a unique ID for accessibility purposes.
* Uses React's useId() for SSR and Concurrent Mode compatibility.
*/
const getUniqueId = (0, react.useCallback)((prefix) => {
return `${prefix}${baseId}`;
}, [baseId]);
return (0, react.useMemo)(() => ({
theme,
resolveString,
resolveNumber,
resolveBoolean,
setValue,
getValue,
sendAction,
getUniqueId
}), [
theme,
resolveString,
resolveNumber,
resolveBoolean,
setValue,
getValue,
sendAction,
getUniqueId
]);
}
//#endregion
//#region src/react-renderer/components/content/Text.tsx
function isHintedStyles(styles) {
if (typeof styles !== "object" || !styles || Array.isArray(styles)) return false;
return [
"h1",
"h2",
"h3",
"h4",
"h5",
"caption",
"body"
].some((v) => v in styles);
}
/**
* Markdown-it instance for rendering markdown text.
* Uses synchronous import to ensure availability at first render (matches Lit renderer).
*
* Configuration matches Lit's markdown directive (uses MarkdownIt defaults):
* - html: false (default) - Security: disable raw HTML
* - linkify: false (default) - Don't auto-convert URLs/emails to links
* - breaks: false (default) - Don't convert \n to <br>
* - typographer: false (default) - Don't use smart quotes/dashes
*/
const markdownRenderer = new markdown_it.default();
/**
* Apply theme classes to markdown HTML elements.
* Replaces default element tags with themed versions.
*/
function applyMarkdownTheme(html, markdownTheme) {
if (!markdownTheme) return html;
const replacements = [];
for (const [element, classes] of Object.entries(markdownTheme)) {
if (!classes || Array.isArray(classes) && classes.length === 0) continue;
const classString = Array.isArray(classes) ? classes.join(" ") : classMapToString(classes);
if (!classString) continue;
const tagRegex = new RegExp(`<${element}(?=\\s|>|/>)`, "gi");
replacements.push([tagRegex, `<${element} class="${classString}"`]);
}
let result = html;
for (const [regex, replacement] of replacements) result = result.replace(regex, replacement);
return result;
}
/**
* Text component - renders text content with markdown support.
*
* Structure mirrors Lit's Text component:
* <div class="a2ui-text"> ← :host equivalent
* <section class="..."> ← theme classes
* <h2>...</h2> ← rendered markdown content
* </section>
* </div>
*
* Text is parsed as markdown and rendered as HTML (matches Lit renderer behavior).
* Supports usageHint values: h1, h2, h3, h4, h5, caption, body
*
* Markdown features supported:
* - **Bold** and *italic* text
* - Lists (ordered and unordered)
* - `inline code` and code blocks
* - [Links](url) (auto-linkified URLs too)
* - Blockquotes
* - Horizontal rules
*
* Note: Raw HTML is disabled for security.
*/
const Text = (0, react.memo)(function Text({ node, surfaceId }) {
var _theme$additionalStyl2;
const { theme, resolveString } = useA2UIComponent(node, surfaceId);
const props = node.properties;
const textValue = resolveString(props.text);
const usageHint = props.usageHint;
const classes = mergeClassMaps(theme.components.Text.all, usageHint ? theme.components.Text[usageHint] : {});
const additionalStyles = (0, react.useMemo)(() => {
var _theme$additionalStyl;
const textStyles = (_theme$additionalStyl = theme.additionalStyles) === null || _theme$additionalStyl === void 0 ? void 0 : _theme$additionalStyl.Text;
if (!textStyles) return void 0;
if (isHintedStyles(textStyles)) return stylesToObject(textStyles[usageHint !== null && usageHint !== void 0 ? usageHint : "body"]);
return stylesToObject(textStyles);
}, [(_theme$additionalStyl2 = theme.additionalStyles) === null || _theme$additionalStyl2 === void 0 ? void 0 : _theme$additionalStyl2.Text, usageHint]);
const renderedContent = (0, react.useMemo)(() => {
if (textValue === null || textValue === void 0) return null;
let markdownText = textValue;
switch (usageHint) {
case "h1":
markdownText = `# ${markdownText}`;
break;
case "h2":
markdownText = `## ${markdownText}`;
break;
case "h3":
markdownText = `### ${markdownText}`;
break;
case "h4":
markdownText = `#### ${markdownText}`;
break;
case "h5":
markdownText = `##### ${markdownText}`;
break;
case "caption":
markdownText = `*${markdownText}*`;
break;
default: break;
}
return { __html: applyMarkdownTheme(markdownRenderer.render(markdownText), theme.markdown) };
}, [
textValue,
theme.markdown,
usageHint
]);
if (!renderedContent) return null;
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
className: "a2ui-text",
style: node.weight !== void 0 ? { "--weight": node.weight } : {},
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
className: classMapToString(classes),
style: additionalStyles,
dangerouslySetInnerHTML: renderedContent
})
});
});
//#endregion
//#region src/react-renderer/components/content/Image.tsx
/**
* Image component - renders an image from a URL with optional sizing and fit modes.
*
* Supports usageHint values: icon, avatar, smallFeature, mediumFeature, largeFeature, header
* Supports fit values: contain, cover, fill, none, scale-down (maps to object-fit via CSS variable)
*/
const Image = (0, react.memo)(function Image({ node, surfaceId }) {
var _ref, _theme$additionalStyl;
const { theme, resolveString } = useA2UIComponent(node, surfaceId);
const props = node.properties;
const rawUrl = resolveString(props.url);
const url = rawUrl === null || rawUrl === void 0 ? void 0 : rawUrl.replace("//via.placeholder.com/", "//placehold.co/");
const usageHint = props.usageHint;
const fit = (_ref = props.fit) !== null && _ref !== void 0 ? _ref : "fill";
const classes = mergeClassMaps(theme.components.Image.all, usageHint ? theme.components.Image[usageHint] : {});
const style = {
...stylesToObject((_theme$additionalStyl = theme.additionalStyles) === null || _theme$additionalStyl === void 0 ? void 0 : _theme$additionalStyl.Image),
"--object-fit": fit
};
if (!url) return null;
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
className: "a2ui-image",
style: node.weight !== void 0 ? { "--weight": node.weight } : {},
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
className: classMapToString(classes),
style,
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
src: url,
alt: ""
})
})
});
});
//#endregion
//#region src/react-renderer/components/content/Icon.tsx
/**
* Convert camelCase to snake_case for Material Symbols font.
* e.g., "shoppingCart" -> "shopping_cart"
* This matches the Lit renderer's approach.
*/
function toSnakeCase(str) {
return str.replace(/([A-Z])/g, "_$1").toLowerCase();
}
/**
* Icon component - renders an icon using Material Symbols Outlined font.
*
* This matches the Lit renderer's approach using the g-icon class with
* Material Symbols Outlined font.
*
* @example Add Material Symbols font to your HTML:
* ```html
* <link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" rel="stylesheet">
* ```
*/
const Icon = (0, react.memo)(function Icon({ node, surfaceId }) {
var _theme$additionalStyl;
const { theme, resolveString } = useA2UIComponent(node, surfaceId);
const props = node.properties;
const iconName = resolveString(props.name);
if (!iconName) return null;
const snakeCaseName = toSnakeCase(iconName);
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
className: "a2ui-icon",
style: node.weight !== void 0 ? { "--weight": node.weight } : {},
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
className: classMapToString(theme.components.Icon),
style: stylesToObject((_theme$additionalStyl = theme.additionalStyles) === null || _theme$additionalStyl === void 0 ? void 0 : _theme$additionalStyl.Icon),
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
className: "g-icon",
children: snakeCaseName
})
})
});
});
//#endregion
//#region src/react-renderer/components/content/Divider.tsx
/**
* Divider component - renders a visual separator line.
*
* Structure mirrors Lit's Divider component:
* <div class="a2ui-divider"> ← :host equivalent
* <hr class="..."> ← internal element
* </div>
*/
const Divider = (0, react.memo)(function Divider({ node, surfaceId }) {
var _theme$additionalStyl;
const { theme } = useA2UIComponent(node, surfaceId);
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
className: "a2ui-divider",
style: node.weight !== void 0 ? { "--weight": node.weight } : {},
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("hr", {
className: classMapToString(theme.components.Divider),
style: stylesToObject((_theme$additionalStyl = theme.additionalStyles) === null || _theme$additionalStyl === void 0 ? void 0 : _theme$additionalStyl.Divider)
})
});
});
//#endregion
//#region src/react-renderer/components/content/Video.tsx
/**
* Check if a URL is a YouTube URL and extract the video ID.
*/
function getYouTubeVideoId(url) {
for (const pattern of [/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([^&\s?]+)/]) {
const match = url.match(pattern);
if (match && match.length > 1) return match[1];
}
return null;
}
/**
* Video component - renders a video player.
*
* Supports regular video URLs and YouTube URLs (renders as embedded iframe).
*/
const Video = (0, react.memo)(function Video({ node, surfaceId }) {
var _theme$additionalStyl;
const { theme, resolveString } = useA2UIComponent(node, surfaceId);
const props = node.properties;
const url = resolveString(props.url);
if (!url) return null;
const youtubeId = getYouTubeVideoId(url);
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
className: "a2ui-video",
style: node.weight !== void 0 ? { "--weight": node.weight } : {},
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
className: classMapToString(theme.components.Video),
style: stylesToObject((_theme$additionalStyl = theme.additionalStyles) === null || _theme$additionalStyl === void 0 ? void 0 : _theme$additionalStyl.Video),
children: youtubeId ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("iframe", {
src: `https://www.youtube.com/embed/${youtubeId}`,
title: "YouTube video player",
allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture",
allowFullScreen: true,
style: {
border: "none",
width: "100%",
aspectRatio: "16/9"
}
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("video", {
src: url,
controls: true
})
})
});
});
//#endregion
//#region src/react-renderer/components/content/AudioPlayer.tsx
/**
* AudioPlayer component - renders an audio player with optional description.
*/
const AudioPlayer = (0, react.memo)(function AudioPlayer({ node, surfaceId }) {
var _props$description, _theme$additionalStyl;
const { theme, resolveString } = useA2UIComponent(node, surfaceId);
const props = node.properties;
const url = resolveString(props.url);
const description = resolveString((_props$description = props.description) !== null && _props$description !== void 0 ? _props$description : null);
if (!url) return null;
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
className: "a2ui-audio",
style: node.weight !== void 0 ? { "--weight": node.weight } : {},
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
className: classMapToString(theme.components.AudioPlayer),
style: stylesToObject((_theme$additionalStyl = theme.additionalStyles) === null || _theme$additionalStyl === void 0 ? void 0 : _theme$additionalStyl.AudioPlayer),
children: [description && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: description }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("audio", {
src: url,
controls: true
})]
})
});
});
//#endregion
//#region src/react-renderer/components/layout/Row.tsx
/**
* Row component - arranges children horizontally using flexbox.
*
* Supports distribution (justify-content) and alignment (align-items) properties.
*/
const Row = (0, react.memo)(function Row({ node, surfaceId }) {
var _props$alignment, _props$distribution, _theme$additionalStyl;
const { theme } = useA2UIComponent(node, surfaceId);
const props = node.properties;
const alignment = (_props$alignment = props.alignment) !== null && _props$alignment !== void 0 ? _props$alignment : "stretch";
const distribution = (_props$distribution = props.distribution) !== null && _props$distribution !== void 0 ? _props$distribution : "start";
const children = Array.isArray(props.children) ? props.children : [];
const hostStyle = node.weight !== void 0 ? { "--weight": node.weight } : {};
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
className: "a2ui-row",
"data-alignment": alignment,
"data-distribution": distribution,
style: hostStyle,
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
className: classMapToString(theme.components.Row),
style: stylesToObject((_theme$additionalStyl = theme.additionalStyles) === null || _theme$additionalStyl === void 0 ? void 0 : _theme$additionalStyl.Row),
children: children.map((child, index) => {
const childId = typeof child === "object" && child !== null && "id" in child ? child.id : `child-${index}`;
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ComponentNode, {
node: typeof child === "object" && child !== null && "type" in child ? child : null,
surfaceId
}, childId);
})
})
});
});
//#endregion
//#region src/react-renderer/components/layout/Column.tsx
/**
* Column component - arranges children vertically using flexbox.
*
* Supports distribution (justify-content) and alignment (align-items) properties.
*/
const Column = (0, react.memo)(function Column({ node, surfaceId }) {
var _props$alignment, _props$distribution, _theme$additionalStyl;
const { theme } = useA2UIComponent(node, surfaceId);
const props = node.properties;
const alignment = (_props$alignment = props.alignment) !== null && _props$alignment !== void 0 ? _props$alignment : "stretch";
const distribution = (_props$distribution = props.distribution) !== null && _props$distribution !== void 0 ? _props$distribution : "start";
const children = Array.isArray(props.children) ? props.children : [];
const hostStyle = node.weight !== void 0 ? { "--weight": node.weight } : {};
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
className: "a2ui-column",
"data-alignment": alignment,
"data-distribution": distribution,
style: hostStyle,
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
className: classMapToString(theme.components.Column),
style: stylesToObject((_theme$additionalStyl = theme.additionalStyles) === null || _theme$additionalStyl === void 0 ? void 0 : _theme$additionalStyl.Column),
children: children.map((child, index) => {
const childId = typeof child === "object" && child !== null && "id" in child ? child.id : `child-${index}`;
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ComponentNode, {
node: typeof child === "object" && child !== null && "type" in child ? child : null,
surfaceId
}, childId);
})
})
});
});
//#endregion
//#region src/react-renderer/components/layout/List.tsx
/**
* List component - renders a scrollable list of items.
*
* Supports direction (vertical/horizontal) properties.
*/
const List = (0, react.memo)(function List({ node, surfaceId }) {
var _props$direction, _theme$additionalStyl;
const { theme } = useA2UIComponent(node, surfaceId);
const props = node.properties;
const direction = (_props$direction = props.direction) !== null && _props$direction !== void 0 ? _props$direction : "vertical";
const children = Array.isArray(props.children) ? props.children : [];
const hostStyle = node.weight !== void 0 ? { "--weight": node.weight } : {};
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
className: "a2ui-list",
"data-direction": direction,
style: hostStyle,
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
className: classMapToString(theme.components.List),
style: stylesToObject((_theme$additionalStyl = theme.additionalStyles) === null || _theme$additionalStyl === void 0 ? void 0 : _theme$additionalStyl.List),
children: children.map((child, index) => {
const childId = typeof child === "object" && child !== null && "id" in child ? child.id : `child-${index}`;
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ComponentNode, {
node: typeof child === "object" && child !== null && "type" in child ? child : null,
surfaceId
}, childId);
})
})
});
});
//#endregion
//#region src/react-renderer/components/layout/Card.tsx
/**
* Card component - a container that visually groups content.
*
* Structure mirrors Lit's Card component:
* <div class="a2ui-card"> ← :host equivalent
* <section class="..."> ← theme classes (border, padding, background)
* {children} ← ::slotted(*) equivalent
* </section>
* </div>
*
* All styles come from componentSpecificStyles CSS, no inline styles needed.
*/
const Card = (0, react.memo)(function Card({ node, surfaceId }) {
var _props$children, _theme$additionalStyl;
const { theme } = useA2UIComponent(node, surfaceId);
const props = node.properties;
const rawChildren = (_props$children = props.children) !== null && _props$children !== void 0 ? _props$children : props.child ? [props.child] : [];
const children = Array.isArray(rawChildren) ? rawChildren : [];
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
className: "a2ui-card",
style: node.weight !== void 0 ? { "--weight": node.weight } : {},
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
className: classMapToString(theme.components.Card),
style: stylesToObject((_theme$additionalStyl = theme.additionalStyles) === null || _theme$additionalStyl === void 0 ? void 0 : _theme$additionalStyl.Card),
children: children.map((child, index) => {
const childId = typeof child === "object" && child !== null && "id" in child ? child.id : `child-${index}`;
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ComponentNode, {
node: typeof child === "object" && child !== null && "type" in child ? child : null,
surfaceId
}, childId);
})
})
});
});
//#endregion
//#region src/react-renderer/components/layout/Tabs.tsx
/**
* Tabs component - displays content in switchable tabs.
*/
const Tabs = (0, react.memo)(function Tabs({ node, surfaceId }) {
var _props$tabItems, _theme$additionalStyl;
const { theme, resolveString } = useA2UIComponent(node, surfaceId);
const props = node.properties;
const [selectedIndex, setSelectedIndex] = (0, react.useState)(0);
const tabItems = (_props$tabItems = props.tabItems) !== null && _props$tabItems !== void 0 ? _props$tabItems : [];
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
className: "a2ui-tabs",
style: node.weight !== void 0 ? { "--weight": node.weight } : {},
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
className: classMapToString(theme.components.Tabs.container),
style: stylesToObject((_theme$additionalStyl = theme.additionalStyles) === null || _theme$additionalStyl === void 0 ? void 0 : _theme$additionalStyl.Tabs),
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
id: "buttons",
className: classMapToString(theme.components.Tabs.element),
children: tabItems.map((tab, index) => {
const title = resolveString(tab.title);
const isSelected = index === selectedIndex;
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
disabled: isSelected,
className: classMapToString(isSelected ? mergeClassMaps(theme.components.Tabs.controls.all, theme.components.Tabs.controls.selected) : theme.components.Tabs.controls.all),
onClick: () => setSelectedIndex(index),
children: title
}, index);
})
}), tabItems[selectedIndex] && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ComponentNode, {
node: tabItems[selectedIndex].child,
surfaceId
})]
})
});
});
//#endregion
//#region src/react-renderer/components/layout/Modal.tsx
/**
* Modal component - displays content in a dialog overlay.
*
* Matches Lit's rendering approach:
* - When closed: renders section with entry point child
* - When open: renders dialog with content child (entry point is replaced)
*
* The dialog is rendered in place (no portal) so it stays inside .a2ui-surface
* and CSS selectors work correctly. showModal() handles the top-layer overlay.
*/
const Modal = (0, react.memo)(function Modal({ node, surfaceId }) {
var _theme$additionalStyl;
const { theme } = useA2UIComponent(node, surfaceId);
const props = node.properties;
const [isOpen, setIsOpen] = (0, react.useState)(false);
const dialogRef = (0, react.useRef)(null);
const openModal = (0, react.useCallback)(() => {
setIsOpen(true);
}, []);
const closeModal = (0, react.useCallback)(() => {
setIsOpen(false);
}, []);
(0, react.useEffect)(() => {
const dialog = dialogRef.current;
if (!dialog) return;
if (isOpen && !dialog.open) dialog.showModal();
const handleClose = () => {
setIsOpen(false);
};
dialog.addEventListener("close", handleClose);
return () => dialog.removeEventListener("close", handleClose);
}, [isOpen]);
const handleBackdropClick = (0, react.useCallback)((e) => {
if (e.target === e.currentTarget) closeModal();
}, [closeModal]);
const handleKeyDown = (0, react.useCallback)((e) => {
if (e.key === "Escape") closeModal();
}, [closeModal]);
const hostStyle = node.weight !== void 0 ? { "--weight": node.weight } : {};
if (!isOpen) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
className: "a2ui-modal",
style: hostStyle,
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
onClick: openModal,
style: { cursor: "pointer" },
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ComponentNode, {
node: props.entryPointChild,
surfaceId
})
})
});
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
className: "a2ui-modal",
style: hostStyle,
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dialog", {
ref: dialogRef,
className: classMapToString(theme.components.Modal.backdrop),
onClick: handleBackdropClick,
onKeyDown: handleKeyDown,
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
className: classMapToString(theme.components.Modal.element),
style: stylesToObject((_theme$additionalStyl = theme.additionalStyles) === null || _theme$additionalStyl === void 0 ? void 0 : _theme$additionalStyl.Modal),
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
id: "controls",
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
onClick: closeModal,
"aria-label": "Close modal",
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
className: "g-icon",
children: "close"
})
})
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ComponentNode, {
node: props.contentChild,
surfaceId
})]
})
})
});
});
//#endregion
//#region src/react-renderer/components/interactive/Button.tsx
/**
* Button component - a clickable element that triggers an action.
*
* Contains a child component (usually Text or Icon) and dispatches
* a user action when clicked.
*/
const Button = (0, react.memo)(function Button({ node, surfaceId }) {
var _theme$additionalStyl;
const { theme, sendAction } = useA2UIComponent(node, surfaceId);
const props = node.properties;
const handleClick = (0, react.useCallback)(() => {
if (props.action) sendAction(props.action);
}, [props.action, sendAction]);
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
className: "a2ui-button",
style: node.weight !== void 0 ? { "--weight": node.weight } : {},
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
className: classMapToString(theme.components.Button),
style: stylesToObject((_theme$additionalStyl = theme.additionalStyles) === null || _theme$additionalStyl === void 0 ? void 0 : _theme$additionalStyl.Button),
onClick: handleClick,
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ComponentNode, {
node: props.child,
surfaceId
})
})
});
});
//#endregion
//#region src/react-renderer/components/interactive/TextField.tsx
/**
* TextField component - an input field for text entry.
*
* Supports various input types and two-way data binding.
*/
const TextField = (0, react.memo)(function TextField({ node, surfaceId }) {
var _props$text, _resolveString, _theme$additionalStyl, _theme$additionalStyl2;
const { theme, resolveString, setValue, getValue } = useA2UIComponent(node, surfaceId);
const props = node.properties;
const id = (0, react.useId)();
const label = resolveString(props.label);
const textPath = (_props$text = props.text) === null || _props$text === void 0 ? void 0 : _props$text.path;
const initialValue = (_resolveString = resolveString(props.text)) !== null && _resolveString !== void 0 ? _resolveString : "";
const fieldType = props.type;
const validationRegexp = props.validationRegexp;
const [value, setLocalValue] = (0, react.useState)(initialValue);
const [_isValid, setIsValid] = (0, react.useState)(true);
(0, react.useEffect)(() => {
if (textPath) {
const externalValue = getValue(textPath);
if (externalValue !== null && String(externalValue) !== value) setLocalValue(String(externalValue));
}
}, [textPath, getValue]);
const handleChange = (0, react.useCallback)((e) => {
const newValue = e.target.value;
setLocalValue(newValue);
if (validationRegexp) setIsValid(new RegExp(validationRegexp).test(newValue));
if (textPath) setValue(textPath, newValue);
}, [
validationRegexp,
textPath,
setValue
]);
const inputType = fieldType === "number" ? "number" : fieldType === "date" ? "date" : "text";
const isTextArea = fieldType === "longText";
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
className: "a2ui-textfield",
style: node.weight !== void 0 ? { "--weight": node.weight } : {},
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
className: classMapToString(theme.components.TextField.container),
children: [label && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
htmlFor: id,
className: classMapToString(theme.components.TextField.label),
children: label
}), isTextArea ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
id,
value,
onChange: handleChange,
placeholder: "Please enter a value",
className: classMapToString(theme.components.TextField.element),
style: stylesToObject((_theme$additionalStyl = theme.additionalStyles) === null || _theme$additionalStyl === void 0 ? void 0 : _theme$additionalStyl.TextField)
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
type: inputType,
id,
value,
onChange: handleChange,
placeholder: "Please enter a value",
className: classMapToString(theme.components.TextField.element),
style: stylesToObject((_theme$additionalStyl2 = theme.additionalStyles) === null || _theme$additionalStyl2 === void 0 ? void 0 : _theme$additionalStyl2.TextField)
})]
})
});
});
//#endregion
//#region src/react-renderer/components/interactive/CheckBox.tsx
/**
* CheckBox component - a boolean toggle with a label.
*
* Supports two-way data binding for the checked state.
*/
const CheckBox = (0, react.memo)(function CheckBox({ node, surfaceId }) {
var _props$value, _resolveBoolean, _props$value3, _theme$additionalStyl;
const { theme, resolveString, resolveBoolean, setValue, getValue } = useA2UIComponent(node, surfaceId);
const props = node.properties;
const id = (0, react.useId)();
const label = resolveString(props.label);
const valuePath = (_props$value = props.value) === null || _props$value === void 0 ? void 0 : _props$value.path;
const [checked, setChecked] = (0, react.useState)((_resolveBoolean = resolveBoolean(props.value)) !== null && _resolveBoolean !== void 0 ? _resolveBoolean : false);
(0, react.useEffect)(() => {
if (valuePath) {
const externalValue = getValue(valuePath);
if (externalValue !== null && Boolean(externalValue) !== checked) setChecked(Boolean(externalValue));
}
}, [valuePath, getValue]);
(0, react.useEffect)(() => {
var _props$value2;
if (((_props$value2 = props.value) === null || _props$value2 === void 0 ? void 0 : _props$value2.literalBoolean) !== void 0) setChecked(props.value.literalBoolean);
}, [(_props$value3 = props.value) === null || _props$value3 === void 0 ? void 0 : _props$value3.literalBoolean]);
const handleChange = (0, react.useCallback)((e) => {
const newValue = e.target.checked;
setChecked(newValue);
if (valuePath) setValue(valuePath, newValue);
}, [valuePath, setValue]);
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
className: "a2ui-checkbox",
style: node.weight !== void 0 ? { "--weight": node.weight } : {},
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
className: classMapToString(theme.components.CheckBox.container),
style: stylesToObject((_theme$additionalStyl = theme.additionalStyles) === null || _theme$additionalStyl === void 0 ? void 0 : _theme$additionalStyl.CheckBox),
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
type: "checkbox",
id,
checked,
onChange: handleChange,
className: classMapToString(theme.components.CheckBox.element)
}), label && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
htmlFor: id,
className: classMapToString(theme.components.CheckBox.label),
children: label
})]
})
});
});
//#endregion
//#region src/react-renderer/components/interactive/Slider.tsx
/**
* Slider component - a numeric value selector with a range.
*
* Supports two-way data binding for the value.
*/
const Slider = (0, react.memo)(function Slider({ node, surfaceId }) {
var _props$value, _resolveNumber, _props$minValue, _props$maxValue, _props$value3, _theme$additionalStyl;
const { theme, resolveNumber, resolveString, setValue, getValue } = useA2UIComponent(node, surfaceId);
const props = node.properties;
const id = (0, react.useId)();
const valuePath = (_props$value = props.value) === null || _props$value === void 0 ? void 0 : _props$value.path;
const initialValue = (_resolveNumber = resolveNumber(props.value)) !== null && _resolveNumber !== void 0 ? _resolveNumber : 0;
const minValue = (_props$minValue = props.minValue) !== null && _props$minValue !== void 0 ? _props$minValue : 0;
const maxValue = (_props$maxValue = props.maxValue) !== null && _props$maxValue !== void 0 ? _props$maxValue : 0;
const [value, setLocalValue] = (0, react.useState)(initialValue);
(0, react.useEffect)(() => {
if (valuePath) {
const externalValue = getValue(valuePath);
if (externalValue !== null && Number(externalValue) !== value) setLocalValue(Number(externalValue));
}
}, [valuePath, getValue]);
(0, react.useEffect)(() => {
var _props$value2;
if (((_props$value2 = props.value) === null || _props$value2 === void 0 ? void 0 : _props$value2.literalNumber) !== void 0) setLocalValue(props.value.literalNumber);
}, [(_props$value3 = props.value) === null || _props$value3 === void 0 ? void 0 : _props$value3.literalNumber]);
const handleChange = (0, react.useCallback)((e) => {
const newValue = Number(e.target.value);
setLocalValue(newValue);
if (valuePath) setValue(valuePath, newValue);
}, [valuePath, setValue]);
const labelValue = props.label;
const label = labelValue ? resolveString(labelValue) : "";
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
className: "a2ui-slider",
style: node.weight !== void 0 ? { "--weight": node.weight } : {},
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
className: classMapToString(theme.components.Slider.container),
children: [
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
htmlFor: id,
className: classMapToString(theme.components.Slider.label),
children: label
}),
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
type: "range",
id,
name: "data",
value,
min: minValue,
max: maxValue,
onChange: handleChange,
className: classMapToString(theme.components.Slider.element),
style: stylesToObject((_theme$additionalStyl = theme.additionalStyles) === null || _theme$additionalStyl === void 0 ? void 0 : _theme$additionalStyl.Slider)
}),
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
className: classMapToString(theme.components.Slider.label),
children: value
})
]
})
});
});
//#endregion
//#region src/react-renderer/components/interactive/DateTimeInput.tsx
/**
* DateTimeInput component - a date and/or time picker.
*
* Supports enabling date, time, or both. Uses native HTML5 date/time inputs.
*/
const DateTimeInput = (0, react.memo)(function DateTimeInput({ node, surfaceId }) {
var _props$value, _resolveString, _props$enableDate, _props$enableTime, _theme$additionalStyl;
const { theme, resolveString, setValue, getValue } = useA2UIComponent(node, surfaceId);
const props = node.properties;
const id = (0, react.useId)();
const valuePath = (_props$value = props.value) === null || _props$value === void 0 ? void 0 : _props$value.path;
const initialValue = (_resolveString = resolveString(props.value)) !== null && _resolveString !== void 0 ? _resolveString : "";
const enableDate = (_props$enableDate = props.enableDate) !== null && _props$enableDate !== void 0 ? _props$enableDate : true;
const enableTime = (_props$enableTime = props.enableTime) !== null && _props$enableTime !== void 0 ? _props$enableTime : false;
const [value, setLocalValue] = (0, react.useState)(initialValue);
(0, react.useEffect)(() => {
if (valuePath) {
const externalValue = getValue(valuePath);
if (externalValue !== null && String(externalValue) !== value) setLocalValue(String(externalValue));
}
}, [valuePath, getValue]);
const handleChange = (0, react.useCallback)((e) => {
const newValue = e.target.value;
setLocalValue(newValue);
if (valuePath) setValue(valuePath, newValue);
}, [valuePath, setValue]);
let inputType = "date";
if (enableDate && enableTime) inputType = "datetime-local";
else if (enableTime && !enableDate) inputType = "time";
const getPlaceholderText = () => {
if (enableDate && enableTime) return "Date & Time";
else if (enableTime) return "Time";
return "Date";
};
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
className: "a2ui-datetime-input",
style: node.weight !== void 0 ? { "--weight": node.weight } : {},
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
className: classMapToString(theme.components.DateTimeInput.container),
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
htmlFor: id,
className: classMapToString(theme.components.DateTimeInput.label),
children: getPlaceholderText()
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
type: inputType,
id,
value,
onChange: handleChange,
className: classMapToString(theme.components.DateTimeInput.element),
style: stylesToObject((_theme$additionalStyl = theme.additionalStyles) === null || _theme$additionalStyl === void 0 ? void 0 : _theme$additionalStyl.DateTimeInput)
})]
})
});
});
//#endregion
//#region src/react-renderer/components/interactive/MultipleChoice.tsx
/**
* MultipleChoice component - a selection component using a dropdown.
*
* Renders a <select> element with options, matching the Lit renderer's behavior.
* Supports two-way data binding for the selected value.
*/
const MultipleChoice = (0, react.memo)(function MultipleChoice({ node, surfaceId }) {
var _ref, _props$selections, _resolveString, _theme$additionalStyl;
const { theme, resolveString, setValue } = useA2UIComponent(node, surfaceId);
const props = node.properties;
const id = (0, react.useId)();
const options = (_ref = props.options) !== null && _ref !== void 0 ? _ref : [];
const selectionsPath = (_props$selections = props.selections) === null || _props$selections === void 0 ? void 0 : _props$selections.path;
const description = (_resolveString = resolveString(props.description)) !== null && _resolveString !== void 0 ? _resolveString : "Select an item";
const handleChange = (0, react.useCallback)((e) => {
if (selectionsPath) setValue(selectionsPath, [e.target.value]);
}, [selectionsPath, setValue]);
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
className: "a2ui-multiplechoice",
style: node.weight !== void 0 ? { "--weight": node.weight } : {},
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
className: classMapToString(theme.components.MultipleChoice.container),
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
htmlFor: id,
className: classMapToString(theme.components.MultipleChoice.label),
children: description
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
name: "data",
id,
className: classMapToString(theme.components.MultipleChoice.element),
style: stylesToObject((_theme$additionalStyl = theme.additionalStyles) === null || _theme$additionalStyl === void 0 ? void 0 : _theme$additionalStyl.MultipleChoice),
onChange: handleChange,
children: options.map((option) => {
const label = resolveString(option.label);
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
value: option.value,
children: label
}, option.value);
})
})]
})
});
});
//#endregion
//#region src/react-renderer/registry/defaultCatalog.ts
/**
* Registers all standard A2UI components in the registry.
*
* @param registry - The component registry to populate
*/
function registerDefaultCatalog(registry) {
registry.register("Text", { component: Text });
registry.register("Image", { component: Image });
registry.register("Icon", { component: Icon });
registry.register("Divider", { component: Divider });
registry.register("Video", { component: Video });
registry.register("AudioPlayer", { component: AudioPlayer });
registry.register("Row", { component: Row });
registry.register("Column", { component: Column });
registry.register("List", { component: List });
registry.register("Card", { component: Card });
registry.register("Tabs", { component: Tabs });
registry.register("Modal", { component: Modal });
registry.register("Button", { component: Button });
registry.register("TextField", { component: TextField });
registry.register("CheckBox", { component: CheckBox });
registry.register("Slider", { component: Slider });
registry.register("DateTimeInput", { component: DateTimeInput });
registry.register("MultipleChoice", { component: MultipleChoice });
}
/**
* Initialize the default catalog in the singleton registry.
* Call this once at app startup.
*/
function initializeDefaultCatalog() {
registerDefaultCatalog(ComponentRegistry.getInstance());
}
//#endregion
//#region src/react-renderer/styles/reset.ts
/**
* Browser default reset for A2UI surfaces.
*
* The React renderer uses Light DOM, which means host-app CSS resets
* (e.g. Tailwind preflight, normalize.css) can strip browser defaults
* like heading margins, list styles, and form element appearance from
* elements inside the renderer.
*
* The Lit renderer avoids this because Shadow DOM isolates its elements
* from external stylesheets.
*
* This reset restores browser defaults inside `.a2ui-surface` by using
* `all: revert` in a CSS @layer. Layered styles have the lowest author
* priority, so every other A2UI style (utility classes, component styles,
* theme classes, inline styles) automatically overrides the reset.
*/
const resetStyles = `
@layer a2ui-reset {
:where(.a2ui-surface) :where(*) {
all: revert;
}
}
`;
//#endregion
//#region src/react-renderer/styles/index.ts
/**
* Structural CSS styles from the Lit renderer, converted for global DOM use.
* These styles define all the utility classes (layout-*, typography-*, color-*, etc.)
* Converts :host selectors to .a2ui-surface for scoped use outside Shadow DOM.
*/
const structuralStyles = _a2ui_lit_0_8.Styles.structuralStyles.replace(/:host\s*\{/g, ".a2ui-surface {");
/**
* Component-specific styles that replicate Lit's Shadow DOM scoped CSS.
*
* Each Lit component has `static styles` with :host, element selectors, and ::slotted().
* Since React uses Light DOM, we transform these to global CSS scoped under .a2ui-surface.
*
* Transformation rules:
* :host → .a2ui-surface .a2ui-{component}
* section → .a2ui-surface .a2ui-{component} section
* ::slotted(*) → .a2ui-surface .a2ui-{component} section > *
*/
const componentSpecificStyles = `
/* =========================================================================
* Card (from Lit card.ts static styles)
* ========================================================================= */
/* :host { display: block; flex: var(--weight); min-height: 0; overflow: auto; } */
.a2ui-surface .a2ui-card {
display: block;
flex: var(--weight);
min-height: 0;
overflow: auto;
}
/* section { height: 100%; width: 100%; min-height: 0; overflow: auto; } */
/* Use > to target only Card's direct section, not nested sections (e.g., TextField's section) */
.a2ui-surface .a2ui-card > section {
height: 100%;
width: 100%;
min-height: 0;
overflow: auto;
}
/* section ::slotted(*) { height: 100%; width: 100%; } */
/* Use > section > to only target Card's slotted children, not deeply nested elements */
.a2ui-surface .a2ui-card > section > * {
height: 100%;
width: 100%;
}
/* =========================================================================
* Divider (from Lit divider.ts static styles)
* ========================================================================= */
/* :host { display: block; min-height: 0; overflow: auto; } */
.a2ui-surface .a2ui-divider {
display: block;
min-height: 0;
overflow: auto;
}
/* hr { height: 1px; background: #ccc; border: none; } */
/* Use :where() for low specificity (0,0,1) so theme utility classes can override */
/* Browser default margins apply (margin-block: 0.5em, margin-inline: auto) */
:where(.a2ui-surface .a2ui-divider) hr {
height: 1px;
background: #ccc;
border: none;
}
/* =========================================================================
* Text (from Lit text.ts static styles)
* ========================================================================= */
/* :host { display: block; flex: var(--weight); } */
.a2ui-surface .a2ui-text {
display: block;
flex: var(--weight);
}
/* h1, h2, h3, h4, h5 { line-height: inherit; font: inherit; } */
/* Use :where() to match Lit's low specificity (0,0,0,1 - just element) */
:where(.a2ui-surface .a2ui-text) h1,
:where(.a2ui-surface .a2ui-text) h2,
:where(.a2ui-surface .a2ui-text) h3,
:where(.a2ui-surface .a2ui-text) h4,
:where(.a2ui-surface .a2ui-text) h5 {
line-height: inherit;
font: inherit;
}
/* Ensure markdown paragraph margins are reset */
.a2ui-surface .a2ui-text p {
margin: 0;
}
/* =========================================================================
* TextField (from Lit text-field.ts static styles)
* ========================================================================= */
/* :host { display: flex; flex: var(--weight); } */
.a2ui-surface .a2ui-textfield {
display: flex;
flex: var(--weight);
}
/* input { display: block; width: 100%; } */
:where(.a2ui-surface .a2ui-textfield) input {
display: block;
width: 100%;
}
/* label { display: block; margin-bottom: 4px; } */
:where(.a2ui-surface .a2ui-textfield) label {
display: block;
margin-bottom: 4px;
}
/* textarea - same styling as input for multiline text fields */
:where(.a2ui-surface .a2ui-textfield) textarea {
display: block;
width: 100%;
}
/* =========================================================================
* CheckBox (from Lit checkbox.ts static styles)
* ========================================================================= */
/* :host { display: block; flex: var(--weight); min-height: 0; overflow: auto; } */
.a2ui-surface .a2ui-checkbox {
display: block;
flex: var(--weight);
min-height: 0;
overflow: auto;
}
/* input { display: block; width: 100%; } */
:where(.a2ui-surface .a2ui-checkbox) input {
display: block;
width: 100%;
}
/* =========================================================================
* Slider (from Lit slider.ts static styles)
* ========================================================================= */
/* :host { display: block; flex: var(--weight); } */
.a2ui-surface .a2ui-slider {
display: block;
flex: var(--weight);
}
/* input { display: block; width: 100%; } */
:where(.a2ui-surface .a2ui-slider) input {
display: block;
width: 100%;
}
/* =========================================================================
* Button (from Lit button.ts static styles)
* ========================================================================= */
/* :host { display: block; flex: var(--weight); min-height: 0; } */
.a2ui-surface .a2ui-button {
display: block;
flex: var(--weight);
min-height: 0;
}
/* =========================================================================
* Icon (from Lit icon.ts static styles)
* ========================================================================= */
/* :host { display: block; flex: var(--weight); min-height: 0; overflow: auto; } */
.a2ui-surface .a2ui-icon {
display: block;
flex: var(--weight);
min-height: 0;
overflow: auto;
}
/* =========================================================================
* Tabs (from Lit tabs.ts static styles)
* ========================================================================= */
/* :host { display: block; flex: var(--weight); } */
.a2ui-surface .a2ui-tabs {
display: block;
flex: var(--weight);
}
/* =========================================================================
* Modal (from Lit modal.ts static styles)
* ========================================================================= */
/* :host { display: block; flex: var(--weight); } */
.a2ui-surface .a2ui-modal {
display: block;
flex: var(--weight);
}
/* dialog { padding: 0; border: none; background: none; } */
:where(.a2ui-surface .a2ui-modal) dialog {
padding: 0;
border: none;
background: none;
}
/* dialog section #controls { display: flex; justify-content: end; margin-bottom: 4px; } */
.a2ui-surface .a2ui-modal dialog section #controls {
display: flex;
justify-content: end;
margin-bottom: 4px;
}
/* dialog section #controls button { padding: 0; background: none; ... } */
.a2ui-surface .a2ui-modal dialog section #controls button {
padding: 0;
background: none;
width: 20px;
height: 20px;
cursor: pointer;
border: none;
}
/* =========================================================================
* Image (from Lit image.ts static styles)
* ========================================================================= */
/* :host { display: block; flex: var(--weight); min-height: 0; overflow: auto; } */
.a2ui-surface .a2ui-image {
display: block;
flex: var(--weight);
min-height: 0;
overflow: auto;
}
/* img { display: block; width: 100%; height: 100%; object-fit: var(--object-fit, fill); } */
:where(.a2ui-surface .a2ui-image) img {
display: block;
width: 100%;
height: 100%;
object-fit: var(--object-fit, fill);
}
/* =========================================================================
* Video (from Lit video.ts static styles)
* ========================================================================= */
/* :host { display: block; flex: var(--weight); min-height: 0; overflow: auto; } */
.a2ui-surface .a2ui-video {
display: block;
flex: var(--weight);
min-height: 0;
overflow: auto;
}
/* video { display: block; width: 100%; } */
:where(.a2ui-surface .a2ui-video) video {
display: block;
width: 100%;
}
/* =========================================================================
* AudioPlayer (from Lit audio.ts static styles)
* ========================================================================= */
/* :host { display: block; flex: var(--weight); min-height: 0; overflow: auto; } */
.a2ui-surface .a2ui-audio {
display: block;
flex: var(--weight);
min-height: 0;
overflow: auto;
}
/* audio { display: block; width: 100%; } */
:where(.a2ui-surface .a2ui-audio) audio {
display: block;
width: 100%;
}
/* =========================================================================
* MultipleChoice (from Lit multiple-choice.ts static styles)
* ========================================================================= */
/* :host { display: block; flex: var(--weight); min-height: 0; overflow: auto; } */
.a2ui-surface .a2ui-multiplechoice {
display: block;
flex: var(--weight);
min-height: 0;
overflow: auto;
}
/* select { width: 100%; } */
:where(.a2ui-surface .a2ui-multiplechoice) select {
width: 100%;
}
/* =========================================================================
* Column (from Lit column.ts static styles)
* ========================================================================= */
/* :host { display: flex; flex: var(--weight); } */
.a2ui-surface .a2ui-column {
display: flex;
flex: var(--weight);
}
/* section { display: flex; flex-direction: column; min-width: 100%; height: 100%; } */
.a2ui-surface .a2ui-column > section {
display: flex;
flex-direction: column;
min-width: 100%;
height: 100%;
}
/* :host([alignment="..."]) section { align-items: ...; } */
/* Use > section to only target Column's direct section, not nested sections (e.g., CheckBox's section) */
.a2ui-surface .a2ui-column[data-alignment="start"] > section { align-items: start; }
.a2ui-surface .a2ui-column[data-alignment="center"] > section { align-items: center; }
.a2ui-surface .a2ui-column[data-alignment="end"] > section { align-items: end; }
.a2ui-surface .a2ui-column[data-alignment="stretch"] > section { align-items: stretch; }
/* :host([distribution="..."]) section { justify-content: ...; } */
.a2ui-surface .a2ui-column[data-distribution="start"] > section { justify-content: start; }
.a2ui-surface .a2ui-column[data-distribution="center"] > section { justify-content: center; }
.a2ui-surface .a2ui-column[data-distribution="end"] > section { justify-content: end; }
.a2ui-surface .a2ui-column[data-distribution="spaceBetween"] > section { justify-content: space-between; }
.a2ui-surface .a2ui-column[data-distribution="spaceAround"] > section { justify-content: space-around; }
.a2ui-surface .a2ui-column[data-distribution="spaceEvenly"] > section { justify-content: space-evenly; }
/* =========================================================================
* Row (from Lit row.ts static styles)
* ========================================================================= */
/* :host { display: flex; flex: var(--weight); } */
.a2ui-surface .a2ui-row {
display: flex;
flex: var(--weight);
}
/* section { display: flex; flex-direction: row; width: 100%; min-height: 100%; } */
.a2ui-surface .a2ui-row > section {
display: flex;
flex-direction: row;
width: 100%;
min-height: 100%;
}
/* :host([alignment="..."]) section { align-items: ...; } */
/* Use > section to only target Row's direct section, not nested sections */
.a2ui-surface .a2ui-row[data-alignment="start"] > section { align-items: start; }
.a2ui-surface .a2ui-row[data-alignment="center"] > section { align-items: center; }
.a2ui-surface .a2ui-row[data-alignment="end"] > section { align-items: end; }
.a2ui-surface .a2ui-row[data-alignment="stretch"] > section { align-items: stretch; }
/* :host([distribution="..."]) section { justify-content: ...; } */
.a2ui-surface .a2ui-row[data-distribution="start"] > section { justify-content: start; }
.a2ui-surface .a2ui-row[data-distribution="center"] > section { justify-content: center; }
.a2ui-surface .a2ui-row[data-distribution="end"] > section { justify-content: end; }
.a2ui-surface .a2ui-row[data-distribution="spaceBetween"] > section { justify-content: space-between; }
.a2ui-surface .a2ui-row[data-distribution="spaceAround"] > section { justify-content: space-around; }
.a2ui-surface .a2ui-row[data-distribution="spaceEvenly"] > section { justify-content: space-evenly; }
/* =========================================================================
* List (from Lit list.ts static styles)
* ========================================================================= */
/* :host { display: block; flex: var(--weight); min-height: 0; overflow: auto; } */
.a2ui-surface .a2ui-list {
display: block;
flex: var(--weight);
min-height: 0;
overflow: auto;
}
/* :host([direction="vertical"]) section { display: grid; } */
.a2ui-surface .a2ui-list[data-direction="vertical"] > section {
display: grid;
}
/* :host([direction="horizontal"]) section { display: flex; max-width: 100%; overflow-x: scroll; ... } */
.a2ui-surface .a2ui-list[data-direction="horizontal"] > section {
display: flex;
max-width: 100%;
overflow-x: scroll;
overflow-y: hidden;
scrollbar-width: none;
}
/* :host([direction="horizontal"]) section > ::slotted(*) { flex: 1 0 fit-content; ... } */
.a2ui-surface .a2ui-list[data-direction="horizontal"] > section > * {
flex: 1 0 fit-content;
max-width: min(80%, 400px);
}
/* =========================================================================
* DateTimeInput (from Lit datetime-input.ts static styles)
* ========================================================================= */
/* :host { display: block; flex: var(--weight); min-height: 0; overflow: auto; } */
.a2ui-surface .a2ui-datetime-input {
display: block;
flex: var(--weight);
min-height: 0;
overflow: auto;
}
/* input { display: block; border-radius: 8px; padding: 8px; border: 1px solid #ccc; width: 100%; } */
/* Use :where() to match Lit's low specificity (0,0,0,1) so theme utility classes can override */
:where(.a2ui-surface .a2ui-datetime-input) input {
display: block;
border-radius: 8px;
padding: 8px;
border: 1px solid #ccc;
width: 100%;
}
/* =========================================================================
* Global box-sizing (matches Lit's * { box-sizing: border-box; } in components)
* ========================================================================= */
.a2ui-surface *,
.a2ui-surface *::before,
.a2ui-surface *::after {
box-sizing: border-box;
}
`;
/**
* Injects A2UI structural styles into the document head.
* Includes utility classes (layout-*, typography-*, color-*, etc.) and React-specific overrides.
* Call this once at application startup.
*
* NOTE: CSS variables (--n-*, --p-*, etc.) must be defined by the host application on :root,
* just like in the Lit renderer. This allows full customization of the color palette.
*
* @example
* ```tsx
* import { injectStyles } from '@a2ui/react/styles';
*
* // In your app entry point:
* injectStyles();
* ```
*/
function injectStyles() {
if (typeof document === "undefined") return;
const styleId = "a2ui-structural-styles";
if (document.getElementById(styleId)) return;
const styleElement = document.createElement("style");
styleElement.id = styleId;
styleElement.textContent = resetStyles + "\n" + structuralStyles + "\n" + componentSpecificStyles;
document.head.appendChild(styleElement);
}
/**
* Removes injected A2UI styles from the document.
* Useful for cleanup in tests or when unmounting.
*/
function removeStyles() {
if (typeof document === "undefined") return;
const styleElement = document.getElementById("a2ui-structural-styles");
if (styleElement) styleElement.remove();
}
//#endregion
//#region src/theme/viewer-theme.ts
/** Elements */
const a = {
"typography-f-sf": true,
"typography-fs-n": true,
"typography-w-500": true,
"layout-as-n": true,
"layout-dis-iflx": true,
"layout-al-c": true
};
const audio = { "layout-w-100": true };
const body = {
"typography-f-s": true,
"typography-fs-n": true,
"typography-w-400": true,
"layout-mt-0": true,
"layout-mb-2": true,
"typography-sz-bm": true,
"color-c-n10": true
};
const button = {
"typography-f-sf": true,
"typography-fs-n": true,
"typography-w-500": true,
"layout-pt-3": true,
"layout-pb-3": true,
"layout-pl-5": true,
"layout-pr-5": true,
"layout-mb-1": true,
"border-br-16": true,
"border-bw-0": true,
"border-c-n70": true,
"border-bs-s": true,
"color-bgc-s30": true,
"color-c-n100": true,
"behavior-ho-80": true
};
const heading = {
"typography-f-sf": true,
"typography-fs-n": true,
"typography-w-500": true,
"layout-mt-0": true,
"layout-mb-2": true,
"color-c-n10": true
};
const h1 = {
...heading,
"typography-sz-tl": true
};
const h2 = {
...heading,
"typography-sz-tm": true
};
const h3 = {
...heading,
"typography-sz-ts": true
};
const iframe = { "behavior-sw-n": true };
const input = {
"typography-f-sf": true,
"typography-fs-n": true,
"typography-w-400": true,
"layout-pl-4": true,
"layout-pr-4": true,
"layout-pt-2": true,
"layout-pb-2": true,
"border-br-6": true,
"border-bw-1": true,
"color-bc-s70": true,
"border-bs-s": true,
"layout-as-n": true,
"color-c-n10": true
};
const p = {
"typography-f-s": true,
"typography-fs-n": true,
"typography-w-400": true,
"layout-m-0": true,
"typography-sz-bm": true,
"layout-as-n": true,
"color-c-n10": true
};
const orderedList = {
"typography-f-s": true,
"typography-fs-n": true,
"typography-w-400": true,
"layout-m-0": true,
"typography-sz-bm": true,
"layout-as-n": true
};
const unorderedList = {
"typography-f-s": true,
"typography-fs-n": true,
"typography-w-400": true,
"layout-m-0": true,
"typography-sz-bm": true,
"layout-as-n": true
};
const listItem = {
"typography-f-s": true,
"typography-fs-n": true,
"typography-w-400": true,
"layout-m-0": true,
"typography-sz-bm": true,
"layout-as-n": true
};
const pre = {
"typography-f-c": true,
"typography-fs-n": true,
"typography-w-400": true,
"typography-sz-bm": true,
"typography-ws-p": true,
"layout-as-n": true
};
const textarea = {
...input,
"layout-r-none": true,
"layout-fs-c": true
};
const video = { "layout-el-cv": true };
const aLight = _a2ui_lit.v0_8.Styles.merge(a, { "color-c-n5": true });
const inputLight = _a2ui_lit.v0_8.Styles.merge(input, { "color-c-n5": true });
const textareaLight = _a2ui_lit.v0_8.Styles.merge(textarea, { "color-c-n5": true });
const buttonLight = _a2ui_lit.v0_8.Styles.merge(button, { "color-c-n100": true });
const h1Light = _a2ui_lit.v0_8.Styles.merge(h1, { "color-c-n5": true });
const h2Light = _a2ui_lit.v0_8.Styles.merge(h2, { "color-c-n5": true });
const h3Light = _a2ui_lit.v0_8.Styles.merge(h3, { "color-c-n5": true });
const bodyLight = _a2ui_lit.v0_8.Styles.merge(body, { "color-c-n5": true });
const pLight = _a2ui_lit.v0_8.Styles.merge(p, { "color-c-n35": true });
const preLight = _a2ui_lit.v0_8.Styles.merge(pre, { "color-c-n35": true });
const orderedListLight = _a2ui_lit.v0_8.Styles.merge(orderedList, { "color-c-n35": true });
const unorderedListLight = _a2ui_lit.v0_8.Styles.merge(unorderedList, { "color-c-n35": true });
const listItemLight = _a2ui_lit.v0_8.Styles.merge(listItem, { "color-c-n35": true });
const theme = {
additionalStyles: {
Button: {
background: "var(--primary, oklch(0.205 0 0))",
color: "var(--primary-foreground, oklch(0.985 0 0))",
"border-radius": "calc(var(--radius, 0.625rem) - 2px)",
cursor: "pointer",
width: "100%",
"--n-10": "var(--primary-foreground, oklch(0.985 0 0))",
"--n-35": "var(--primary-foreground, oklch(0.985 0 0))",
"--n-60": "var(--primary-foreground, oklch(0.985 0 0))"
},
Card: {
background: "var(--card, oklch(1 0 0))",
border: "1px solid var(--border, oklch(0.922 0 0))",
"border-radius": "var(--radius, 0.625rem)",
padding: "16px"
},
TextField: {
"background-color": "var(--background, oklch(1 0 0))",
"border-color": "var(--input, oklch(0.922 0 0))",
color: "var(--foreground, oklch(0.145 0 0))",
"border-radius": "var(--radius, 0.625rem)"
},
CheckBox: {
"--n-100": "var(--background, oklch(1 0 0))",
"--n-70": "var(--border, oklch(0.922 0 0))",
"--n-30": "var(--foreground, oklch(0.145 0 0))"
},
DateTimeInput: {
"background-color": "var(--background, oklch(1 0 0))",
"border-color": "var(--input, oklch(0.922 0 0))",
color: "var(--foreground, oklch(0.145 0 0))",
"border-radius": "var(--radius, 0.625rem)"
},
Modal: {
"--p-100": "var(--card, oklch(1 0 0))",
"--p-80": "var(--border, oklch(0.922 0 0))",
"border-radius": "var(--radius, 0.625rem)"
},
Text: { color: "var(--foreground, oklch(0.145 0 0))" }
},
components: {
AudioPlayer: {},
Button: {
"layout-pt-2": true,
"layout-pb-2": true,
"layout-pl-3": true,
"layout-pr-3": true,
"border-bw-0": true,
"border-bs-s": true
},
Card: {
"border-br-9": true,
"color-bgc-n100": true
},
CheckBox: {
element: {
"layout-m-0": true,
"layout-mr-2": true,
"layout-p-2": true,
"border-br-2": true,
"border-bw-1": true,
"border-bs-s": true,
"color-bgc-n100": true,
"color-bc-n70": true,
"color-c-n30": true
},
label: {
"typography-f-sf": true,
"typography-v-r": true,
"typography-w-400": true,
"layout-flx-1": true,
"typography-sz-ll": true
},
container: {
"layout-dsp-iflex": true,
"layout-al-c": true
}
},
Column: { "layout-g-2": true },
DateTimeInput: {
container: {},
label: {},
element: {
"layout-pt-2": true,
"layout-pb-2": true,
"layout-pl-3": true,
"layout-pr-3": true,
"border-br-2": true,
"border-bw-1": true,
"border-bs-s": true,
"color-bgc-n100": true,
"color-bc-n70": true,
"color-c-n30": true
}
},
Divider: {},
Image: {
all: {
"border-br-5": true,
"layout-el-cv": true,
"layout-w-100": true,
"layout-h-100": true
},
avatar: { "is-avatar": true },
header: {},
icon: {},
largeFeature: {},
mediumFeature: {},
smallFeature: {}
},
Icon: {},
List: {
"layout-g-4": true,
"layout-p-2": true
},
Modal: {
backdrop: { "color-bbgc-n10_20": true },
element: {
"border-br-2": true,
"color-bgc-n100": true,
"layout-p-4": true,
"border-bw-1": true,
"border-bs-s": true,
"color-bc-n80": true
}
},
MultipleChoice: {
container: {},
label: {},
element: {}
},
Row: { "layout-g-4": true },
Slider: {
container: {},
label: {},
element: {}
},
Tabs: {
container: {},
controls: {
all: {},
selected: {}
},
element: {}
},
Text: {
all: {
"layout-w-100": true,
"layout-g-2": true
},
h1: {
"typography-f-sf": true,
"typography-v-r": true,
"typography-w-400": true,
"layout-m-0": true,
"layout-p-0": true,
"typography-sz-tl": true
},
h2: {
"typography-f-sf": true,
"typography-v-r": true,
"typography-w-400": true,
"layout-m-0": true,
"layout-p-0": true,
"typography-sz-tm": true
},
h3: {
"typography-f-sf": true,
"typography-v-r": true,
"typography-w-400": true,
"layout-m-0": true,
"layout-p-0": true,
"typography-sz-ts": true
},
h4: {
"typography-f-sf": true,
"typography-v-r": true,
"typography-w-400": true,
"layout-m-0": true,
"layout-p-0": true,
"typography-sz-bl": true
},
h5: {
"typography-f-sf": true,
"typography-v-r": true,
"typography-w-400": true,
"layout-m-0": true,
"layout-p-0": true,
"typography-sz-bm": true
},
body: {},
caption: {}
},
TextField: {
container: {
"typography-sz-bm": true,
"layout-w-100": true,
"layout-g-2": true,
"layout-dsp-flexvert": true
},
label: { "layout-flx-0": true },
element: {
"typography-sz-bm": true,
"layout-pt-2": true,
"layout-pb-2": true,
"layout-pl-3": true,
"layout-pr-3": true,
"border-br-2": true,
"border-bw-1": true,
"border-bs-s": true,
"color-bgc-n100": true,
"color-bc-n70": true,
"color-c-n30": true
}
},
Video: {
"border-br-5": true,
"layout-el-cv": true
}
},
elements: {
a: aLight,
audio,
body: bodyLight,
button: buttonLight,
em: { "typography-fs-n": true },
h1: h1Light,
h2: h2Light,
h3: h3Light,
h4: {},
h5: {},
iframe,
input: inputLight,
p: pLight,
pre: preLight,
textarea: textareaLight,
video
},
markdown: {
p: [...Object.keys(pLight)],
h1: [...Object.keys(h1Light)],
h2: [...Object.keys(h2Light)],
h3: [...Object.keys(h3Light)],
h4: [],
h5: [],
ul: [...Object.keys(unorderedListLight)],
ol: [...Object.keys(orderedListLight)],
li: [...Object.keys(listItemLight)],
a: [...Object.keys(aLight)],
strong: [],
em: ["typography-fs-n"]
}
};
//#endregion
//#region src/A2UIViewer.tsx
let initialized = false;
function ensureInitialized() {
if (!initialized) {
initializeDefaultCatalog();
injectStyles();
initialized = true;
}
}
/**
* A2UIViewer renders an A2UI component tree from a JSON definition and data.
* It re-renders cleanly when props change, discarding previous state.
*/
function A2UIViewer({ root, components, data, onAction, styles, className }) {
ensureInitialized();
const baseId = (0, react.useId)();
const surfaceId = (0, react.useMemo)(() => {
const definitionKey = `${root}-${JSON.stringify(components)}`;
let hash = 0;
for (let i = 0; i < definitionKey.length; i++) {
const char = definitionKey.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash;
}
return `surface${baseId.replace(/:/g, "-")}${hash}`;
}, [
baseId,
root,
components
]);
const handleAction = (0, react.useMemo)(() => {
if (!onAction) return void 0;
return (message) => {
const userAction = message.userAction;
if (userAction) {
var _userAction$context;
onAction({
actionName: userAction.name,
sourceComponentId: userAction.sourceComponentId,
timestamp: userAction.timestamp,
context: (_userAction$context = userAction.context) !== null && _userAction$context !== void 0 ? _userAction$context : {}
});
}
};
}, [onAction]);
if (!components || components.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
className,
style: {
padding: 16,
color: "#666",
fontFamily: "system-ui"
},
children: "No content to display"
});
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(A2UIProvider, {
onAction: handleAction,
theme,
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(A2UIViewerInner, {
surfaceId,
root,
components,
data: data !== null && data !== void 0 ? data : {},
styles,
className
})
});
}
/**
* Inner component that processes messages within the provider context.
*/
function A2UIViewerInner({ surfaceId, root, components, data, styles, className }) {
const { processMessages } = useA2UIActions();
const lastProcessedRef = (0, react.useRef)("");
(0, react.useEffect)(() => {
const key = `${surfaceId}-${JSON.stringify(components)}-${JSON.stringify(data)}`;
if (key === lastProcessedRef.current) return;
lastProcessedRef.current = key;
const messages = [{ beginRendering: {
surfaceId,
root,
styles: styles !== null && styles !== void 0 ? styles : {}
} }, { surfaceUpdate: {
surfaceId,
components
} }];
if (data && Object.keys(data).length > 0) {
const contents = objectToValueMaps(data);
if (contents.length > 0) messages.push({ dataModelUpdate: {
surfaceId,
path: "/",
contents
} });
}
processMessages(messages);
}, [
processMessages,
surfaceId,
root,
components,
data,
styles
]);
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
className,
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(A2UIRenderer, { surfaceId })
});
}
/**
* Converts a nested JavaScript object to the ValueMap[] format
* expected by A2UI's dataModelUpdate message.
*/
function objectToValueMaps(obj) {
return Object.entries(obj).map(([key, value]) => valueToValueMap(key, value));
}
/**
* Converts a single key-value pair to a ValueMap.
*/
function valueToValueMap(key, value) {
if (typeof value === "string") return {
key,
valueString: value
};
if (typeof value === "number") return {
key,
valueNumber: value
};
if (typeof value === "boolean") return {
key,
valueBoolean: value
};
if (value === null || value === void 0) return { key };
if (Array.isArray(value)) return {
key,
valueMap: value.map((item, index) => valueToValueMap(String(index), item))
};
if (typeof value === "object") return {
key,
valueMap: objectToValueMaps(value)
};
return { key };
}
//#endregion
//#region src/a2ui-types.ts
const DEFAULT_SURFACE_ID = _a2ui_lit.v0_8.Data.A2uiMessageProcessor.DEFAULT_SURFACE_ID;
//#endregion
exports.A2UIProvider = A2UIProvider;
exports.A2UIRenderer = A2UIRenderer;
exports.A2UIViewer = A2UIViewer;
exports.AudioPlayer = AudioPlayer;
exports.Button = Button;
exports.Card = Card;
exports.CheckBox = CheckBox;
exports.Column = Column;
exports.ComponentNode = ComponentNode;
exports.ComponentRegistry = ComponentRegistry;
exports.DEFAULT_SURFACE_ID = DEFAULT_SURFACE_ID;
exports.DateTimeInput = DateTimeInput;
exports.Divider = Divider;
exports.Icon = Icon;
exports.Image = Image;
exports.List = List;
exports.Modal = Modal;
exports.MultipleChoice = MultipleChoice;
exports.Row = Row;
exports.Slider = Slider;
exports.Tabs = Tabs;
exports.Text = Text;
exports.TextField = TextField;
exports.ThemeProvider = ThemeProvider;
exports.Video = Video;
exports.classMapToString = classMapToString;
exports.cn = cn;
exports.defaultTheme = defaultTheme;
exports.initializeDefaultCatalog = initializeDefaultCatalog;
exports.injectStyles = injectStyles;
exports.litTheme = litTheme;
exports.registerDefaultCatalog = registerDefaultCatalog;
exports.removeStyles = removeStyles;
exports.stylesToObject = stylesToObject;
exports.useA2UI = useA2UI;
exports.useA2UIActions = useA2UIActions;
exports.useA2UIComponent = useA2UIComponent;
exports.useA2UIContext = useA2UIContext;
exports.useA2UIState = useA2UIState;
exports.useA2UIStore = useA2UIStore;
exports.useA2UIStoreSelector = useA2UIStoreSelector;
exports.useTheme = useTheme;
exports.useThemeOptional = useThemeOptional;
exports.viewerTheme = theme;
});
//# sourceMappingURL=index.umd.js.map