Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/a2ui-catalog.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ Only `@a2uiCatalog` is a custom tag. Use standard TypeDoc-supported comments and

Keep built-in catalog CSS assets in `packages/genui/a2ui/styles/catalog/*.css`, not under `src/catalog`. Catalog component TSX files should import those assets through paths that stay valid after TypeScript emits `dist/catalog/<Component>/index.jsx`, for example `../../../styles/catalog/Button.css`.

When evolving `packages/genui/a2ui-playground`, treat protocol-prefixed hashes such as `#/a2ui/...` and `#/openui/...` as the canonical routes, and preserve the current mainline tab names (`create`, `examples`, `components`) when adding protocol-aware routing. If you keep compatibility aliases for older or transitional paths such as `#/demos` or `#/chat`, parse them into the canonical route model instead of letting a rebase silently rename the mainline routes.

When a GenUI package builds a CLI or other generated artifact that another workspace package executes during its own build, declare that package's `dist/**` (or equivalent generated directory) as Turbo `build.outputs`. Without explicit outputs, cache hits can skip restoring the built CLI and leave downstream workspace bins pointing at missing files.
2 changes: 1 addition & 1 deletion packages/genui/a2ui-playground/lynx.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default defineConfig({
],
source: {
entry: {
main: './lynx-src/index.tsx',
a2ui: './lynx-src/a2ui/index.tsx',
},
},
Comment thread
gaoachao marked this conversation as resolved.
environments: {
Expand Down
111 changes: 88 additions & 23 deletions packages/genui/a2ui-playground/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,70 @@
// 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, useLayoutEffect, useState } from 'react';
import {
useCallback,
useEffect,
useLayoutEffect,
useMemo,
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';
import { OpenUIComponentsPage } from './pages/OpenUIComponentsPage.js';
import { OpenUIDemosPage } from './pages/OpenUIDemosPage.js';
import type { Protocol, ProtocolName } from './utils/protocol.js';
import { DEFAULT_PROTOCOL, getProtocol } from './utils/protocol.js';

type Tab = 'create' | 'examples' | 'components';

const TABS: { id: Tab; label: string }[] = [
interface TabDef {
id: Tab;
label: string;
}

const A2UI_TABS: TabDef[] = [
{ id: 'create', label: 'Create' },
{ id: 'examples', label: 'Examples' },
{ id: 'components', label: 'Components' },
];

const OPENUI_TABS: TabDef[] = [
{ id: 'examples', label: 'Examples' },
{ id: 'components', label: 'Components' },
];

interface Route {
protocol: Protocol;
tab: Tab;
componentName?: string;
}

function parseHash(hash: string): Route {
const cleaned = hash.replace(/^#\/?/u, '');
const parts = cleaned.split('/');
if (parts[0] === 'examples' || parts[0] === 'demos') {
return { tab: 'examples' };

let protocol: Protocol = DEFAULT_PROTOCOL;
let rest = parts;

if (parts[0] === 'a2ui' || parts[0] === 'openui') {
protocol = getProtocol(parts[0]);
rest = parts.slice(1);
}
if (parts[0] === 'create' || parts[0] === 'chat') {
return { tab: 'create' };

if (rest[0] === 'demos' || rest[0] === 'examples') {
return { protocol, tab: 'examples' };
}
if (rest[0] === 'components') {
return { protocol, tab: 'components', componentName: rest[1] };
}
if (parts[0] === 'components') {
return { tab: 'components', componentName: parts[1] };
if (rest[0] === 'chat' || rest[0] === 'create') {
return { protocol, tab: 'create' };
}
return { tab: 'create' };
// OpenUI has no create tab, default to examples.
if (protocol.name === 'openui') return { protocol, tab: 'examples' };
return { protocol, tab: 'create' };
Comment thread
gaoachao marked this conversation as resolved.
}

type Theme = 'light' | 'dark';
Expand All @@ -54,9 +83,11 @@ export function App() {
const [route, setRoute] = useState<Route>(() =>
parseHash(window.location.hash)
);
const [protocol, setProtocol] = useState<ProtocolVersion>(DEFAULT_PROTOCOL);
const [theme, setTheme] = useState<Theme>(getSystemTheme);

const protocol = route.protocol;
const tabs = protocol.name === 'openui' ? OPENUI_TABS : A2UI_TABS;

useLayoutEffect(() => {
document.documentElement.setAttribute('data-theme', theme);
}, [theme]);
Expand All @@ -70,10 +101,33 @@ export function App() {
}, []);

const handleTabClick = useCallback((id: Tab) => {
window.location.hash = `#/${id}`;
}, []);
window.location.hash = `#/${protocol.name}/${id}`;
}, [protocol.name]);

const handleProtocolSelect = useCallback((name: ProtocolName) => {
// When switching to openui and current tab is create, fallback to examples.
const tab = name === 'openui' && route.tab === 'create'
? 'examples'
: route.tab;
window.location.hash = `#/${name}/${tab}`;
}, [route.tab]);

const page = useMemo(() => {
if (protocol.name === 'openui') {
switch (route.tab) {
case 'components':
return (
<OpenUIComponentsPage
key='openui-components'
protocol={protocol}
componentName={route.componentName}
/>
);
default:
return <OpenUIDemosPage key='openui-examples' protocol={protocol} />;
}
}

const page = (() => {
switch (route.tab) {
case 'examples':
return <DemosPage key='examples' protocol={protocol} />;
Expand All @@ -88,15 +142,29 @@ export function App() {
default:
return <AIChatPage key='create' protocol={protocol} />;
}
})();
}, [protocol, route.tab, route.componentName]);

const protocolVersionControl = (
<div className='protocolControl'>
<div className='protocolLabel'>Protocol</div>
<select
className='protocolSelect'
value={protocol.name}
onChange={(e) => handleProtocolSelect(e.target.value as ProtocolName)}
>
Comment thread
gaoachao marked this conversation as resolved.
<option value='a2ui'>A2UI v0.9</option>
<option value='openui'>OpenUI v0.1</option>
</select>
</div>
);

return (
<div className='appShell'>
<div className='topBar'>
<span className='brand'>Lynx A2UI Playground</span>
<span className='brand'>Lynx GenUI Playground</span>

<nav className='tabNav'>
{TABS.map((t) => (
{tabs.map((t) => (
<button
key={t.id}
type='button'
Expand All @@ -112,10 +180,7 @@ export function App() {

<div className='spacer' />

<div className='protocolControl'>
<div className='protocolLabel'>Protocol</div>
<ProtocolSwitch value={protocol} onChange={setProtocol} />
</div>
{protocolVersionControl}

<button
type='button'
Expand Down
36 changes: 23 additions & 13 deletions packages/genui/a2ui-playground/src/componentCatalog.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// 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';
import type { ProtocolName } from './utils/protocol.js';

export interface ComponentProp {
name: string;
Expand All @@ -17,7 +17,7 @@ export interface ComponentDoc {
category: ComponentCategory;
description: string;
props: ComponentProp[];
usage: Record<ProtocolVersion, object>;
usage: Record<ProtocolName, object>;
}

export const CATEGORIES: { id: ComponentCategory; label: string }[] = [
Expand Down Expand Up @@ -46,12 +46,13 @@ export const COMPONENT_CATALOG: ComponentDoc[] = [
},
],
usage: {
'0.9': {
a2ui: {
id: 'greeting',
component: 'Text',
variant: 'h2',
text: 'Hello, world!',
},
openui: {},
},
},
{
Expand All @@ -76,12 +77,13 @@ export const COMPONENT_CATALOG: ComponentDoc[] = [
},
],
usage: {
'0.9': {
a2ui: {
id: 'submit-btn',
component: 'Button',
action: { event: { name: 'submit' } },
child: 'submit-btn-text',
},
openui: {},
},
},
{
Expand All @@ -108,13 +110,14 @@ export const COMPONENT_CATALOG: ComponentDoc[] = [
},
],
usage: {
'0.9': {
id: 'lynx-image',
a2ui: {
id: 'hero-image',
component: 'Image',
url: 'https://picsum.photos/seed/a2ui-image-preview/320/180',
fit: 'cover',
variant: 'mediumFeature',
},
openui: {},
},
},
{
Expand All @@ -130,11 +133,12 @@ export const COMPONENT_CATALOG: ComponentDoc[] = [
},
],
usage: {
'0.9': {
a2ui: {
id: 'section-divider',
component: 'Divider',
axis: 'horizontal',
},
openui: {},
},
},
{
Expand All @@ -149,11 +153,12 @@ export const COMPONENT_CATALOG: ComponentDoc[] = [
},
],
usage: {
'0.9': {
a2ui: {
id: 'info-card',
component: 'Card',
child: 'info-card-content',
},
openui: {},
},
},
{
Expand Down Expand Up @@ -182,13 +187,14 @@ export const COMPONENT_CATALOG: ComponentDoc[] = [
},
],
usage: {
'0.9': {
a2ui: {
id: 'action-row',
component: 'Row',
align: 'center',
justify: 'spaceBetween',
children: ['left-item', 'right-item'],
},
openui: {},
},
},
{
Expand Down Expand Up @@ -217,13 +223,14 @@ export const COMPONENT_CATALOG: ComponentDoc[] = [
},
],
usage: {
'0.9': {
a2ui: {
id: 'main-column',
component: 'Column',
align: 'start',
justify: 'start',
children: ['header', 'body', 'footer'],
},
openui: {},
},
},
{
Expand All @@ -250,13 +257,14 @@ export const COMPONENT_CATALOG: ComponentDoc[] = [
},
],
usage: {
'0.9': {
a2ui: {
id: 'item-list',
component: 'List',
direction: 'vertical',
align: 'stretch',
children: ['item-1', 'item-2', 'item-3'],
},
openui: {},
},
},
{
Expand All @@ -278,12 +286,13 @@ export const COMPONENT_CATALOG: ComponentDoc[] = [
},
],
usage: {
'0.9': {
a2ui: {
id: 'agree-checkbox',
component: 'CheckBox',
label: 'I agree to the terms',
value: false,
},
openui: {},
},
},
{
Expand All @@ -310,13 +319,14 @@ export const COMPONENT_CATALOG: ComponentDoc[] = [
},
],
usage: {
'0.9': {
a2ui: {
id: 'size-picker',
component: 'RadioGroup',
items: ['Small', 'Medium', 'Large'],
value: 'Medium',
usageHint: 'card',
},
openui: {},
},
},
];
13 changes: 5 additions & 8 deletions packages/genui/a2ui-playground/src/components/ProtocolSwitch.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
// 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';
import type { Protocol } from '../utils/protocol.js';

export function ProtocolSwitch(props: {
value: ProtocolVersion;
onChange: (next: ProtocolVersion) => void;
protocol: Protocol;
}) {
const { value, onChange } = props;
const { protocol } = props;
return (
<div className='protocolSwitch' role='group' aria-label='Protocol version'>
<button
type='button'
className={value === '0.9' ? 'protocolButton active' : 'protocolButton'}
onClick={() =>
onChange('0.9')}
className='protocolButton active'
>
v0.9
{protocol.name === 'a2ui' ? 'A2UI' : 'OpenUI'} v{protocol.version}
</button>
</div>
);
Expand Down
Loading
Loading