) {
* const { theme, resolveString, setValue } = useA2UIComponent(node, surfaceId);
*
* const label = resolveString(node.properties.label);
* const value = resolveString(node.properties.text) ?? '';
*
* return (
*
*
* setValue(node.properties.text?.path!, e.target.value)}
* />
*
* );
* }
* ```
*/
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
* - 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:
* ← :host equivalent
* ← theme classes
* ...
← rendered markdown content
*
*
*
* 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
*
* ```
*/
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:
* ← :host equivalent
*
← internal element
*
*/
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:
* ← :host equivalent
* ← theme classes (border, padding, background)
* {children} ← ::slotted(*) equivalent
*
*
*
* 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