From 1d913db13cd237e551c65c570481a4d4e8d58bbc Mon Sep 17 00:00:00 2001 From: Sherry-hue <37186915+Sherry-hue@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:37:31 +0800 Subject: [PATCH] feat: add A2UI playground init feat: add playground new style feat: add lynx web project feat: add json style feat: use 0.9 mock data feat: add data demo feat: add citywalk demo feat: add grid demo feat: add weight prop && grid fix: remove 0.9 --- .../genui/a2ui-playground/lynx-src/App.tsx | 263 +++++ .../genui/a2ui-playground/lynx-src/index.css | 4 + .../genui/a2ui-playground/lynx-src/index.tsx | 9 + .../a2ui-playground/lynx-src/tsconfig.json | 12 + packages/genui/a2ui-playground/lynx.config.ts | 37 + packages/genui/a2ui-playground/package.json | 38 + .../genui/a2ui-playground/rsbuild.config.ts | 28 + packages/genui/a2ui-playground/src/App.tsx | 104 ++ .../a2ui-playground/src/componentCatalog.ts | 328 ++++++ .../a2ui-playground/src/components/Chip.tsx | 8 + .../src/components/MobilePreview.tsx | 20 + .../src/components/ProtocolSwitch.tsx | 23 + .../a2ui-playground/src/components/QrCode.tsx | 59 + .../src/components/UsageSection.tsx | 96 ++ packages/genui/a2ui-playground/src/demos.ts | 122 ++ packages/genui/a2ui-playground/src/entry.tsx | 15 + .../genui/a2ui-playground/src/globals.d.ts | 14 + .../a2ui-playground/src/mock/EventSource.ts | 133 +++ .../src/mock/messages/cast-grid.json | 151 +++ .../src/mock/messages/citywalk-list.json | 254 ++++ .../src/mock/messages/fridge-search.json | 400 +++++++ .../src/mock/messages/recs.json | 208 ++++ .../src/mock/messages/trip-planner.json | 373 ++++++ .../src/mock/messages/workout-plan.json | 431 +++++++ .../a2ui-playground/src/pages/AIChatPage.tsx | 133 +++ .../src/pages/ComponentsPage.tsx | 147 +++ .../a2ui-playground/src/pages/DemosPage.tsx | 238 ++++ .../a2ui-playground/src/pages/Dynamic.tsx | 278 +++++ .../genui/a2ui-playground/src/pages/Home.tsx | 64 + .../a2ui-playground/src/pages/Static.tsx | 79 ++ packages/genui/a2ui-playground/src/render.tsx | 132 +++ packages/genui/a2ui-playground/src/styles.css | 1045 +++++++++++++++++ .../a2ui-playground/src/utils/base64url.ts | 20 + .../a2ui-playground/src/utils/demoUrl.ts | 4 + .../a2ui-playground/src/utils/protocol.ts | 12 + .../a2ui-playground/src/utils/renderUrl.ts | 28 + packages/genui/a2ui-playground/tsconfig.json | 14 + .../genui/a2ui/src/catalog/Column/index.tsx | 15 + .../genui/a2ui/src/catalog/Image/style.css | 6 + .../genui/a2ui/src/catalog/List/index.tsx | 4 +- packages/genui/a2ui/src/catalog/Row/index.tsx | 15 + packages/genui/a2ui/src/catalog/all.ts | 21 +- pnpm-lock.yaml | 288 +++++ 43 files changed, 5661 insertions(+), 12 deletions(-) create mode 100644 packages/genui/a2ui-playground/lynx-src/App.tsx create mode 100644 packages/genui/a2ui-playground/lynx-src/index.css create mode 100644 packages/genui/a2ui-playground/lynx-src/index.tsx create mode 100644 packages/genui/a2ui-playground/lynx-src/tsconfig.json create mode 100644 packages/genui/a2ui-playground/lynx.config.ts create mode 100644 packages/genui/a2ui-playground/package.json create mode 100644 packages/genui/a2ui-playground/rsbuild.config.ts create mode 100644 packages/genui/a2ui-playground/src/App.tsx create mode 100644 packages/genui/a2ui-playground/src/componentCatalog.ts create mode 100644 packages/genui/a2ui-playground/src/components/Chip.tsx create mode 100644 packages/genui/a2ui-playground/src/components/MobilePreview.tsx create mode 100644 packages/genui/a2ui-playground/src/components/ProtocolSwitch.tsx create mode 100644 packages/genui/a2ui-playground/src/components/QrCode.tsx create mode 100644 packages/genui/a2ui-playground/src/components/UsageSection.tsx create mode 100644 packages/genui/a2ui-playground/src/demos.ts create mode 100644 packages/genui/a2ui-playground/src/entry.tsx create mode 100644 packages/genui/a2ui-playground/src/globals.d.ts create mode 100755 packages/genui/a2ui-playground/src/mock/EventSource.ts create mode 100644 packages/genui/a2ui-playground/src/mock/messages/cast-grid.json create mode 100644 packages/genui/a2ui-playground/src/mock/messages/citywalk-list.json create mode 100644 packages/genui/a2ui-playground/src/mock/messages/fridge-search.json create mode 100644 packages/genui/a2ui-playground/src/mock/messages/recs.json create mode 100644 packages/genui/a2ui-playground/src/mock/messages/trip-planner.json create mode 100644 packages/genui/a2ui-playground/src/mock/messages/workout-plan.json create mode 100644 packages/genui/a2ui-playground/src/pages/AIChatPage.tsx create mode 100644 packages/genui/a2ui-playground/src/pages/ComponentsPage.tsx create mode 100644 packages/genui/a2ui-playground/src/pages/DemosPage.tsx create mode 100644 packages/genui/a2ui-playground/src/pages/Dynamic.tsx create mode 100644 packages/genui/a2ui-playground/src/pages/Home.tsx create mode 100644 packages/genui/a2ui-playground/src/pages/Static.tsx create mode 100644 packages/genui/a2ui-playground/src/render.tsx create mode 100644 packages/genui/a2ui-playground/src/styles.css create mode 100644 packages/genui/a2ui-playground/src/utils/base64url.ts create mode 100644 packages/genui/a2ui-playground/src/utils/demoUrl.ts create mode 100644 packages/genui/a2ui-playground/src/utils/protocol.ts create mode 100644 packages/genui/a2ui-playground/src/utils/renderUrl.ts create mode 100644 packages/genui/a2ui-playground/tsconfig.json diff --git a/packages/genui/a2ui-playground/lynx-src/App.tsx b/packages/genui/a2ui-playground/lynx-src/App.tsx new file mode 100644 index 0000000000..d07c042362 --- /dev/null +++ b/packages/genui/a2ui-playground/lynx-src/App.tsx @@ -0,0 +1,263 @@ +// Copyright 2026 The Lynx Authors. All rights reserved. +// Licensed under the Apache License Version 2.0 that can be found in the +// LICENSE file in the root directory of this source tree. +import { A2UIRender, BaseClient } from '@lynx-js/a2ui-reactlynx/core'; +import type { Resource } from '@lynx-js/a2ui-reactlynx/core'; +import '@lynx-js/a2ui-reactlynx/catalog/all'; +import { + useEffect, + useInitData, + useMemo, + useRef, + useState, +} from '@lynx-js/react'; + +interface InitData { + messagesUrl?: string; + messages?: unknown; + actionMocksUrl?: string; + actionMocks?: unknown; +} + +type A2uiMessage = Record & { messageId?: string }; + +type ActionMocks = Record; + +type ResponseMessages = A2uiMessage[]; + +const STREAM_MESSAGE_DELAY_MS = 800; + +function randomId(prefix: string) { + return prefix + Date.now().toString(36) + + Math.random().toString(36).slice(2, 10); +} + +function normalizePayloadToMessages(payload: unknown): ResponseMessages { + if (payload === null || payload === undefined) { + return []; + } + + if (Array.isArray(payload)) { + return payload as ResponseMessages; + } + + if (typeof payload === 'string') { + try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const parsed = JSON.parse(payload); + return normalizePayloadToMessages(parsed); + } catch { + return []; + } + } + + if ( + typeof payload === 'object' + && Array.isArray((payload as Record).messages) + ) { + return (payload as Record).messages as ResponseMessages; + } + + return []; +} + +async function loadMessages(initData: InitData): Promise { + if (initData.messagesUrl) { + // eslint-disable-next-line n/no-unsupported-features/node-builtins + const res = await fetch(initData.messagesUrl, { cache: 'no-store' }); + const text = await res.text(); + try { + return normalizePayloadToMessages(JSON.parse(text)); + } catch { + return normalizePayloadToMessages(text); + } + } + + if (initData.messages !== undefined) { + return normalizePayloadToMessages(initData.messages); + } + + return []; +} + +async function loadActionMocks(initData: InitData): Promise { + if (initData.actionMocksUrl) { + // eslint-disable-next-line n/no-unsupported-features/node-builtins + const res = await fetch(initData.actionMocksUrl, { cache: 'no-store' }); + const text = await res.text(); + try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const parsed = JSON.parse(text); + if (parsed && typeof parsed === 'object') { + return parsed as ActionMocks; + } + return {}; + } catch { + return {}; + } + } + + if (initData.actionMocks && typeof initData.actionMocks === 'object') { + return initData.actionMocks as ActionMocks; + } + + return {}; +} + +export function App() { + const rawInitData = useInitData(); + + const initData = useMemo(() => { + if (typeof rawInitData === 'string') { + try { + return JSON.parse(rawInitData) as InitData; + } catch { + return {} as InitData; + } + } + return (rawInitData ?? {}) as InitData; + }, [rawInitData]); + + // biome-ignore lint/suspicious/noExplicitAny: + const clientRef = useRef(null); + + const [resource, setResource] = useState(null); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); + + useEffect(() => { + let cancelled = false; + + const run = async () => { + setLoading(true); + setError(''); + + const [rawMessages, actionMocks] = await Promise.all([ + loadMessages(initData ?? {}), + loadActionMocks(initData ?? {}), + ]); + + const messageId = randomId('demo_'); + const messages = rawMessages.map((msg) => ({ + ...msg, + messageId: messageId, + })); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const client = clientRef.current ?? new BaseClient(''); + + clientRef.current ??= client; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + client.processUserAction = async ( + userAction: Record, + ) => { + const name = userAction?.name as string | undefined; + if (!name || !actionMocks[name]) { + return []; + } + + const rawResponseMessages = normalizePayloadToMessages( + actionMocks[name], + ); + const actionMessageId = randomId('action_'); + const responseMessages = rawResponseMessages.map((msg) => ({ + ...msg, + messageId: actionMessageId, + })); + + void (async () => { + for (const msg of responseMessages) { + if (cancelled) break; + // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access + client.processor?.processMessages?.([msg]); + await new Promise((resolve) => + setTimeout(resolve, STREAM_MESSAGE_DELAY_MS) + ); + } + })(); + + return responseMessages; + }; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access + client.processor?.clearSurfaces?.(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access + client.resources?.clear?.(); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access + const { resource: newResource } = await client.send( + '' as unknown, + messageId, + ); + // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access + client.resources?.set?.(messageId, newResource); + + if (!cancelled) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + setResource(newResource); + } + + const simulateStream = async () => { + for (const msg of messages) { + if (cancelled) break; + // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access + client.processor?.processMessages?.([msg]); + await new Promise((resolve) => + setTimeout(resolve, STREAM_MESSAGE_DELAY_MS) + ); + } + }; + + void simulateStream(); + }; + + run() + .catch((e) => { + if (!cancelled) { + setError(String(e)); + setResource(null); + } + }) + .finally(() => { + if (!cancelled) { + setLoading(false); + } + }); + + return () => { + cancelled = true; + }; + }, [initData]); + + return ( + + {error + ? ( + + {error} + + ) + : null} + + {loading + ? ( + + Loading... + + ) + : null} + + {resource + ? ( + + + + ) + : null} + + ); +} diff --git a/packages/genui/a2ui-playground/lynx-src/index.css b/packages/genui/a2ui-playground/lynx-src/index.css new file mode 100644 index 0000000000..047d6a50a4 --- /dev/null +++ b/packages/genui/a2ui-playground/lynx-src/index.css @@ -0,0 +1,4 @@ +page { + padding: 10px; + box-sizing: border-box; +} diff --git a/packages/genui/a2ui-playground/lynx-src/index.tsx b/packages/genui/a2ui-playground/lynx-src/index.tsx new file mode 100644 index 0000000000..3f88998c88 --- /dev/null +++ b/packages/genui/a2ui-playground/lynx-src/index.tsx @@ -0,0 +1,9 @@ +// Copyright 2026 The Lynx Authors. All rights reserved. +// Licensed under the Apache License Version 2.0 that can be found in the +// LICENSE file in the root directory of this source tree. +import { root } from '@lynx-js/react'; + +import { App } from './App.jsx'; +import './index.css'; + +root.render(); diff --git a/packages/genui/a2ui-playground/lynx-src/tsconfig.json b/packages/genui/a2ui-playground/lynx-src/tsconfig.json new file mode 100644 index 0000000000..7d8eed26ef --- /dev/null +++ b/packages/genui/a2ui-playground/lynx-src/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "composite": true, + "jsx": "react-jsx", + "jsxImportSource": "@lynx-js/react", + "module": "ESNext", + "moduleResolution": "Bundler", + "noEmit": true, + }, + "include": ["./**/*.ts", "./**/*.tsx"], +} diff --git a/packages/genui/a2ui-playground/lynx.config.ts b/packages/genui/a2ui-playground/lynx.config.ts new file mode 100644 index 0000000000..f00e5e4324 --- /dev/null +++ b/packages/genui/a2ui-playground/lynx.config.ts @@ -0,0 +1,37 @@ +// Copyright 2026 The Lynx Authors. All rights reserved. +// Licensed under the Apache License Version 2.0 that can be found in the +// LICENSE file in the root directory of this source tree. + +import { pluginQRCode } from '@lynx-js/qrcode-rsbuild-plugin'; +import { pluginReactLynx } from '@lynx-js/react-rsbuild-plugin'; +import { defineConfig } from '@lynx-js/rspeedy'; + +export default defineConfig({ + plugins: [ + pluginQRCode({ + schema(url) { + return { + default: `${url}?fullscreen=true`, + }; + }, + }), + pluginReactLynx({ + defaultDisplayLinear: false, + }), + ], + source: { + entry: { + main: './lynx-src/index.tsx', + }, + }, + environments: { + web: {}, + lynx: {}, + }, + output: { + distPath: { + root: 'www', + }, + filename: '[name].[platform].js', + }, +}); diff --git a/packages/genui/a2ui-playground/package.json b/packages/genui/a2ui-playground/package.json new file mode 100644 index 0000000000..584e995056 --- /dev/null +++ b/packages/genui/a2ui-playground/package.json @@ -0,0 +1,38 @@ +{ + "name": "a2ui-playground", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "rsbuild build", + "build:lynx": "rspeedy build", + "dev": "rsbuild dev", + "dev:lynx": "rspeedy dev", + "preview": "rsbuild preview", + "preview:lynx": "rspeedy preview" + }, + "dependencies": { + "@codemirror/lang-json": "^6.0.2", + "@lynx-js/a2ui-reactlynx": "workspace:*", + "@lynx-js/lynx-core": "0.1.3", + "@lynx-js/react": "workspace:*", + "@lynx-js/web-core": "workspace:*", + "@lynx-js/web-elements": "workspace:*", + "@uiw/react-codemirror": "^4.25.9", + "qrcode": "^1.5.4", + "react": "^19.2.4", + "react-dom": "^19.2.4" + }, + "devDependencies": { + "@lynx-js/qrcode-rsbuild-plugin": "workspace:*", + "@lynx-js/react-rsbuild-plugin": "workspace:*", + "@lynx-js/rspeedy": "workspace:*", + "@lynx-js/types": "3.7.0", + "@rsbuild/core": "catalog:rsbuild", + "@rsbuild/plugin-react": "^1.4.5", + "@types/qrcode": "^1.5.5", + "@types/react": "npm:@types/react@^19.2.14", + "@types/react-dom": "^19.2.3", + "typescript": "^5.9.3" + } +} diff --git a/packages/genui/a2ui-playground/rsbuild.config.ts b/packages/genui/a2ui-playground/rsbuild.config.ts new file mode 100644 index 0000000000..f2d4487d21 --- /dev/null +++ b/packages/genui/a2ui-playground/rsbuild.config.ts @@ -0,0 +1,28 @@ +// Copyright 2026 The Lynx Authors. All rights reserved. +// Licensed under the Apache License Version 2.0 that can be found in the +// LICENSE file in the root directory of this source tree. +import { defineConfig } from '@rsbuild/core'; +import { pluginReact } from '@rsbuild/plugin-react'; + +export default defineConfig({ + plugins: [pluginReact()], + source: { + entry: { + index: './src/entry.tsx', + render: './src/render.tsx', + }, + }, + server: { + host: '0.0.0.0', + cors: { + origin: '*', + }, + publicDir: [ + { + name: 'www', + copyOnBuild: false, + watch: true, + }, + ], + }, +}); diff --git a/packages/genui/a2ui-playground/src/App.tsx b/packages/genui/a2ui-playground/src/App.tsx new file mode 100644 index 0000000000..a17502a822 --- /dev/null +++ b/packages/genui/a2ui-playground/src/App.tsx @@ -0,0 +1,104 @@ +// Copyright 2026 The Lynx Authors. All rights reserved. +// Licensed under the Apache License Version 2.0 that can be found in the +// LICENSE file in the root directory of this source tree. +import { useCallback, useEffect, useState } from 'react'; + +import { ProtocolSwitch } from './components/ProtocolSwitch.js'; +import { AIChatPage } from './pages/AIChatPage.js'; +import { ComponentsPage } from './pages/ComponentsPage.js'; +import { DemosPage } from './pages/DemosPage.js'; +import type { ProtocolVersion } from './utils/protocol.js'; +import { DEFAULT_PROTOCOL } from './utils/protocol.js'; + +type Tab = 'chat' | 'demos' | 'components'; + +const TABS: { id: Tab; label: string }[] = [ + { id: 'chat', label: 'AI Chat' }, + { id: 'demos', label: 'Demos' }, + { id: 'components', label: 'Components' }, +]; + +interface Route { + tab: Tab; + componentName?: string; +} + +function parseHash(hash: string): Route { + const cleaned = hash.replace(/^#\/?/u, ''); + const parts = cleaned.split('/'); + if (parts[0] === 'demos') return { tab: 'demos' }; + if (parts[0] === 'components') { + return { tab: 'components', componentName: parts[1] }; + } + return { tab: 'chat' }; +} + +export function App() { + const [route, setRoute] = useState(() => + parseHash(window.location.hash) + ); + const [protocol, setProtocol] = useState(DEFAULT_PROTOCOL); + + useEffect(() => { + const onHashChange = () => { + setRoute(parseHash(window.location.hash)); + }; + window.addEventListener('hashchange', onHashChange); + return () => window.removeEventListener('hashchange', onHashChange); + }, []); + + const handleTabClick = useCallback((id: Tab) => { + window.location.hash = `#/${id}`; + }, []); + + const page = (() => { + switch (route.tab) { + case 'demos': + return ; + case 'components': + return ( + + ); + default: + return ; + } + })(); + + return ( +
+
+ A2UI Playground + + + +
+ +
+
Protocol
+ +
+
+ +
+ {page} +
+
+ ); +} diff --git a/packages/genui/a2ui-playground/src/componentCatalog.ts b/packages/genui/a2ui-playground/src/componentCatalog.ts new file mode 100644 index 0000000000..2577abaa6c --- /dev/null +++ b/packages/genui/a2ui-playground/src/componentCatalog.ts @@ -0,0 +1,328 @@ +// Copyright 2026 The Lynx Authors. All rights reserved. +// Licensed under the Apache License Version 2.0 that can be found in the +// LICENSE file in the root directory of this source tree. +import type { ProtocolVersion } from './utils/protocol.js'; + +export interface ComponentProp { + name: string; + type: string; + description: string; + default?: string; +} + +export type ComponentCategory = 'Display' | 'Layout' | 'Input' | 'Data'; + +export interface ComponentDoc { + name: string; + category: ComponentCategory; + description: string; + props: ComponentProp[]; + usage: Record; +} + +export const CATEGORIES: { id: ComponentCategory; label: string }[] = [ + { id: 'Display', label: 'Display' }, + { id: 'Layout', label: 'Layout' }, + { id: 'Input', label: 'Input' }, + { id: 'Data', label: 'Data' }, +]; + +export const COMPONENT_CATALOG: ComponentDoc[] = [ + { + name: 'Text', + category: 'Display', + description: 'Displays a text string with optional style variant.', + props: [ + { + name: 'text', + type: 'TextValue', + description: 'Content to display', + }, + { + name: 'usageHint', + type: 'string', + description: 'Text style variant: h1, h2, h3, body, caption', + default: 'body', + }, + ], + usage: { + '0.9': { + id: 'greeting', + component: 'Text', + variant: 'h2', + text: 'Hello, world!', + }, + }, + }, + { + name: 'Button', + category: 'Input', + description: 'An interactive button that triggers an action when pressed.', + props: [ + { + name: 'child', + type: 'string', + description: 'ID of the child component rendered inside the button', + }, + { + name: 'action', + type: 'object', + description: 'Action to trigger on press', + }, + ], + usage: { + '0.9': { + id: 'submit-btn', + component: 'Button', + action: { event: { name: 'submit' } }, + child: 'submit-btn-text', + }, + }, + }, + { + name: 'Image', + category: 'Display', + description: 'Displays an image from a URL with optional dimensions.', + props: [ + { name: 'src', type: 'string', description: 'Image URL' }, + { + name: 'alt', + type: 'string', + description: 'Alternative text for accessibility', + }, + { + name: 'width', + type: 'number', + description: 'Image width in logical pixels', + }, + { + name: 'height', + type: 'number', + description: 'Image height in logical pixels', + }, + ], + usage: { + '0.9': { + id: 'hero-image', + component: 'Image', + src: 'https://example.com/hero.png', + alt: 'Hero banner', + width: 320, + height: 180, + }, + }, + }, + { + name: 'Divider', + category: 'Display', + description: 'A visual separator line used to divide content sections.', + props: [ + { + name: 'direction', + type: 'string', + description: 'Divider orientation: horizontal, vertical', + default: 'horizontal', + }, + { + name: 'color', + type: 'string', + description: 'Divider line color as a CSS color value', + }, + ], + usage: { + '0.9': { + id: 'section-divider', + component: 'Divider', + direction: 'horizontal', + color: '#E0E0E0', + }, + }, + }, + { + name: 'Card', + category: 'Layout', + description: 'A container with visual elevation or outline styling.', + props: [ + { + name: 'child', + type: 'string', + description: 'ID of the child component rendered inside the card', + }, + { + name: 'usageHint', + type: 'string', + description: 'Card style: elevated, outlined, filled', + default: 'elevated', + }, + ], + usage: { + '0.9': { + id: 'info-card', + component: 'Card', + usageHint: 'elevated', + child: 'info-card-content', + }, + }, + }, + { + name: 'Row', + category: 'Layout', + description: + 'A horizontal layout container that arranges children in a row.', + props: [ + { + name: 'children', + type: 'ComponentArrayReference', + description: 'Child components arranged horizontally', + }, + { + name: 'alignment', + type: 'string', + description: 'Vertical alignment: start, center, end, stretch', + default: 'center', + }, + { + name: 'distribution', + type: 'string', + description: + 'Horizontal distribution: start, center, end, spaceBetween, spaceAround, spaceEvenly', + default: 'start', + }, + ], + usage: { + '0.9': { + id: 'action-row', + component: 'Row', + alignment: 'center', + distribution: 'spaceBetween', + children: ['left-item', 'right-item'], + }, + }, + }, + { + name: 'Column', + category: 'Layout', + description: + 'A vertical layout container that arranges children in a column.', + props: [ + { + name: 'children', + type: 'ComponentArrayReference', + description: 'Child components arranged vertically', + }, + { + name: 'usageHint', + type: 'string', + description: 'Column role: root-column, group', + default: 'group', + }, + ], + usage: { + '0.9': { + id: 'main-column', + component: 'Column', + usageHint: 'root-column', + children: ['header', 'body', 'footer'], + }, + }, + }, + { + name: 'List', + category: 'Data', + description: + 'A scrollable list that renders children or uses a template for dynamic items.', + props: [ + { + name: 'children', + type: 'ComponentArrayReference', + description: 'Child components or template for list items', + }, + { + name: 'template', + type: 'object', + description: 'Template for dynamic item rendering with data binding', + }, + ], + usage: { + '0.9': { + id: 'item-list', + component: 'List', + children: ['item-1', 'item-2', 'item-3'], + template: { + dataSource: '/items', + component: 'Text', + variant: 'body', + text: { path: '/items/$/name' }, + }, + }, + }, + }, + { + name: 'CheckBox', + category: 'Input', + description: 'A toggleable checkbox with label and action support.', + props: [ + { + name: 'label', + type: 'string', + description: 'Label text displayed next to the checkbox', + }, + { + name: 'checked', + type: 'boolean', + description: 'Whether the checkbox is checked', + default: 'false', + }, + { + name: 'action', + type: 'object', + description: 'Action triggered when the checkbox is toggled', + }, + ], + usage: { + '0.9': { + id: 'agree-checkbox', + component: 'CheckBox', + label: 'I agree to the terms', + checked: false, + action: { event: { name: 'toggle_agree' } }, + }, + }, + }, + { + name: 'RadioGroup', + category: 'Input', + description: + 'A group of mutually exclusive radio options with selection support.', + props: [ + { + name: 'options', + type: 'array', + description: 'List of radio options with label and value', + }, + { + name: 'selected', + type: 'string', + description: 'Value of the currently selected option', + }, + { + name: 'action', + type: 'object', + description: 'Action triggered when the selection changes', + }, + ], + usage: { + '0.9': { + id: 'size-picker', + component: 'RadioGroup', + options: [ + { label: 'Small', value: 'sm' }, + { label: 'Medium', value: 'md' }, + { label: 'Large', value: 'lg' }, + ], + selected: 'md', + action: { event: { name: 'select_size' } }, + }, + }, + }, +]; diff --git a/packages/genui/a2ui-playground/src/components/Chip.tsx b/packages/genui/a2ui-playground/src/components/Chip.tsx new file mode 100644 index 0000000000..bbe41d0153 --- /dev/null +++ b/packages/genui/a2ui-playground/src/components/Chip.tsx @@ -0,0 +1,8 @@ +// Copyright 2026 The Lynx Authors. All rights reserved. +// Licensed under the Apache License Version 2.0 that can be found in the +// LICENSE file in the root directory of this source tree. +import type React from 'react'; + +export function Chip(props: { children: React.ReactNode }) { + return {props.children}; +} diff --git a/packages/genui/a2ui-playground/src/components/MobilePreview.tsx b/packages/genui/a2ui-playground/src/components/MobilePreview.tsx new file mode 100644 index 0000000000..6b040ef1f4 --- /dev/null +++ b/packages/genui/a2ui-playground/src/components/MobilePreview.tsx @@ -0,0 +1,20 @@ +// Copyright 2026 The Lynx Authors. All rights reserved. +// Licensed under the Apache License Version 2.0 that can be found in the +// LICENSE file in the root directory of this source tree. +export function MobilePreview(props: { src: string }) { + return ( +
+
+
+
+
+
+