3086 lines
103 KiB
JavaScript
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
|