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
5 changes: 5 additions & 0 deletions .changeset/tabs-catalog-and-playground.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lynx-js/a2ui-reactlynx": patch
---

Add the `Tabs` catalog component, generate its `catalog.json` schema, and export the new catalog entry and subpath from `@lynx-js/a2ui-reactlynx`.
7 changes: 6 additions & 1 deletion packages/genui/a2ui-playground/lynx-src/a2ui/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
List,
RadioGroup,
Row,
Tabs,
Text,
createMessageStore,
normalizePayloadToMessages as normalizeProtocolMessages,
Expand All @@ -35,6 +36,7 @@ import imageManifest from '@lynx-js/a2ui-reactlynx/catalog/Image/catalog.json';
import listManifest from '@lynx-js/a2ui-reactlynx/catalog/List/catalog.json';
import radioGroupManifest from '@lynx-js/a2ui-reactlynx/catalog/RadioGroup/catalog.json';
import rowManifest from '@lynx-js/a2ui-reactlynx/catalog/Row/catalog.json';
import tabsManifest from '@lynx-js/a2ui-reactlynx/catalog/Tabs/catalog.json';
import textManifest from '@lynx-js/a2ui-reactlynx/catalog/Text/catalog.json';
import {
useCallback,
Expand Down Expand Up @@ -79,6 +81,7 @@ const ALL_BUILTINS: readonly CatalogInput[] = [
manifestEntry(Icon, iconManifest),
manifestEntry(CheckBox, checkBoxManifest),
manifestEntry(RadioGroup, radioGroupManifest),
manifestEntry(Tabs, tabsManifest),
];

interface InitData {
Expand Down Expand Up @@ -336,7 +339,9 @@ export function App() {
() => streamConfig.theme ?? 'light',
[streamConfig.theme],
);
const themeClassName = theme === 'dark' ? 'luna-dark' : 'luna-light';
const themeClassName = theme === 'dark'
? 'luna-dark a2ui-dark'
: 'luna-light a2ui-light';
const isPlaybackPaused = useMemo(
() => effectiveData.playbackPaused === true,
[effectiveData.playbackPaused],
Expand Down
36 changes: 16 additions & 20 deletions packages/genui/a2ui-playground/lynx-src/a2ui/index.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

58 changes: 57 additions & 1 deletion packages/genui/a2ui-playground/src/catalog/a2ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import imageManifest from '@lynx-js/a2ui-reactlynx/catalog/Image/catalog.json';
import listManifest from '@lynx-js/a2ui-reactlynx/catalog/List/catalog.json';
import radioGroupManifest from '@lynx-js/a2ui-reactlynx/catalog/RadioGroup/catalog.json';
import rowManifest from '@lynx-js/a2ui-reactlynx/catalog/Row/catalog.json';
import tabsManifest from '@lynx-js/a2ui-reactlynx/catalog/Tabs/catalog.json';
import textManifest from '@lynx-js/a2ui-reactlynx/catalog/Text/catalog.json';

import type { ProtocolName } from './utils/protocol.js';
Expand Down Expand Up @@ -51,6 +52,17 @@ function inferType(prop: PropSchema): string {
if (Array.isArray(prop.oneOf)) {
return (prop.oneOf as PropSchema[]).map((v) => inferType(v)).join(' | ');
}
if (prop.type === 'array') {
const items = prop.items as PropSchema | undefined;
if (items && items.type === 'object' && items.properties) {
const fields = Object.entries(
items.properties as Record<string, PropSchema>,
).map(([name, schema]) => `${name}: ${inferType(schema)}`);
return `Array<{ ${fields.join('; ')} }>`;
}
if (items) return `${inferType(items)}[]`;
return 'unknown[]';
}
Comment thread
HuJean marked this conversation as resolved.
if (prop.type === 'string') {
if (Array.isArray(prop.enum)) {
return (prop.enum as string[]).map((v) => `"${v}"`).join(' | ');
Expand All @@ -59,7 +71,6 @@ function inferType(prop: PropSchema): string {
}
if (prop.type === 'boolean') return 'boolean';
if (prop.type === 'number') return 'number';
if (prop.type === 'array') return 'string[]';
if (prop.type === 'object') {
const props = prop.properties as Record<string, unknown> | undefined;
if (!props) return 'object';
Expand Down Expand Up @@ -547,6 +558,51 @@ export const COMPONENT_CATALOG: ComponentDoc[] = [
openui: [],
},
},
{
name: 'Tabs',
category: 'Layout',
description: 'A tab bar that switches between multiple child components.',
props: schemaToProps(tabsManifest),
usage: {
a2ui: {
id: 'details-tabs',
component: 'Tabs',
tabs: [
{ title: 'Overview', child: 'tabs-overview' },
{ title: 'Specs', child: 'tabs-specs' },
],
},
openui: {},
},
usageExamples: {
a2ui: [
{
label: 'Simple',
value: {
id: 'details-tabs',
component: 'Tabs',
tabs: [
{ title: 'Overview', child: 'tabs-overview' },
{ title: 'Specs', child: 'tabs-specs' },
],
},
},
{
label: 'Three tabs',
value: {
id: 'details-tabs-3',
component: 'Tabs',
tabs: [
{ title: 'Overview', child: 'tabs-overview' },
{ title: 'Specs', child: 'tabs-specs' },
{ title: 'Reviews', child: 'tabs-reviews' },
],
},
},
],
openui: [],
},
},
{
name: 'RadioGroup',
category: 'Input',
Expand Down
2 changes: 1 addition & 1 deletion packages/genui/a2ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ This package includes:
components they want available.
- `catalog/<Name>`: built-in component renderers (`Text`, `Button`,
`Card`, `Column`, `Row`, `List`, `CheckBox`, `RadioGroup`, `Image`,
`Divider`).
`Divider`, `Icon`, `Tabs`).
- `catalog/<Name>/catalog.json`: per-component JSON-Schema manifests
for the agent handshake.

Expand Down
5 changes: 5 additions & 0 deletions packages/genui/a2ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@
"default": "./dist/catalog/RadioGroup/index.js"
},
"./catalog/RadioGroup/catalog.json": "./dist/catalog/RadioGroup/catalog.json",
"./catalog/Tabs": {
"types": "./dist/catalog/Tabs/index.d.ts",
"default": "./dist/catalog/Tabs/index.js"
},
"./catalog/Tabs/catalog.json": "./dist/catalog/Tabs/catalog.json",
"./react": {
"types": "./dist/react/index.d.ts",
"default": "./dist/react/index.js"
Expand Down
14 changes: 12 additions & 2 deletions packages/genui/a2ui/src/catalog/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
The package intentionally **does not** ship an "all-in-one" catalog
constant. A top-level array referencing every built-in defeats
tree-shaking — every consumer of such an aggregate would bundle every
component, even the nine you don't use. Composition is per-component, and
the cost is visible at the import site.
component, even the components you don't use. Composition is per-component,
and the cost is visible at the import site.

## The minimum a renderer needs

Expand Down Expand Up @@ -72,10 +72,12 @@ import {
CheckBox,
Column,
Divider,
Icon,
Image,
List,
RadioGroup,
Row,
Tabs,
Text,
} from '@lynx-js/a2ui-reactlynx';
import buttonManifest from '@lynx-js/a2ui-reactlynx/catalog/Button/catalog.json' with {
Expand All @@ -93,6 +95,9 @@ import columnManifest from '@lynx-js/a2ui-reactlynx/catalog/Column/catalog.json'
import dividerManifest from '@lynx-js/a2ui-reactlynx/catalog/Divider/catalog.json' with {
type: 'json',
};
import iconManifest from '@lynx-js/a2ui-reactlynx/catalog/Icon/catalog.json' with {
type: 'json',
};
import imageManifest from '@lynx-js/a2ui-reactlynx/catalog/Image/catalog.json' with {
type: 'json',
};
Expand All @@ -105,6 +110,9 @@ import radioGroupManifest from '@lynx-js/a2ui-reactlynx/catalog/RadioGroup/catal
import rowManifest from '@lynx-js/a2ui-reactlynx/catalog/Row/catalog.json' with {
type: 'json',
};
import tabsManifest from '@lynx-js/a2ui-reactlynx/catalog/Tabs/catalog.json' with {
type: 'json',
};
import textManifest from '@lynx-js/a2ui-reactlynx/catalog/Text/catalog.json' with {
type: 'json',
};
Expand All @@ -119,7 +127,9 @@ export const allBuiltins = defineCatalog([
[Button, buttonManifest],
[Divider, dividerManifest],
[CheckBox, checkBoxManifest],
[Icon, iconManifest],
[RadioGroup, radioGroupManifest],
[Tabs, tabsManifest],
]);
```

Expand Down
89 changes: 89 additions & 0 deletions packages/genui/a2ui/src/catalog/Tabs/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// 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 { useState } from '@lynx-js/react';

import { NodeRenderer } from '../../react/A2UIRenderer.jsx';
import type { GenericComponentProps, Surface } from '../../store/types.js';

import '../../../styles/catalog/Tabs.css';

/**
* @a2uiCatalog Tabs
*/
export interface TabsProps extends GenericComponentProps {
tabs: Array<{
title: string;
child: string;
}>;
}

function TabsHeader(props: {
active: boolean;
onSelect: () => void;
tab: {
title: string;
child: string;
};
}): import('@lynx-js/react').ReactNode {
return (
<view
className={`tabs-header${props.active ? ' tabs-header-active' : ''}`}
bindtap={props.onSelect}
>
<text className='tabs-header-text'>{props.tab.title}</text>
</view>
);
}

function TabsContent(props: {
activeTab:
| {
title: string;
child: string;
}
| undefined;
surface: Surface;
}): import('@lynx-js/react').ReactNode {
const childId = props.activeTab?.child;
if (!childId) return null;

const child = props.surface.components.get(childId);
if (!child) return null;

return <NodeRenderer component={child} surface={props.surface} />;
}

export function Tabs(props: TabsProps): import('@lynx-js/react').ReactNode {
const { surface, tabs } = props;
const [selectedIndex, setSelectedIndex] = useState(0);
const activeIndex = tabs.length > 0
? Math.min(selectedIndex, tabs.length - 1)
: 0;
const activeTab = tabs[activeIndex];

if (tabs.length === 0) {
return <view className='tabs' />;
}

return (
<view className='tabs'>
<view className='tabs-headers'>
{tabs.map((tab, index) => (
<TabsHeader
key={`${index}-${tab.child}`}
active={index === activeIndex}
onSelect={() => setSelectedIndex(index)}
tab={tab}
/>
))}
</view>
<view className='tabs-content'>
<TabsContent
activeTab={activeTab}
surface={surface}
/>
</view>
</view>
);
}
1 change: 1 addition & 0 deletions packages/genui/a2ui/src/catalog/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@ export { Image } from './Image/index.jsx';
export { List } from './List/index.jsx';
export { RadioGroup } from './RadioGroup/index.jsx';
export { Row } from './Row/index.jsx';
export { Tabs } from './Tabs/index.jsx';
export { Text } from './Text/index.jsx';
1 change: 1 addition & 0 deletions packages/genui/a2ui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export {
List,
RadioGroup,
Row,
Tabs,
Text,
Icon,
} from './catalog/index.js';
10 changes: 5 additions & 5 deletions packages/genui/a2ui/styles/catalog/Button.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
gap: var(--a2ui-spacing-m);
padding: var(--a2ui-spacing-m) calc(var(--a2ui-spacing-m) * 1.75);
margin: calc(var(--a2ui-spacing-xs) * 2) 0;
border: var(--a2ui-border-width) solid var(--a2ui-color-border);
border: var(--a2ui-button-border);
border-radius: var(--a2ui-button-border-radius, var(--a2ui-border-radius));
background: var(--a2ui-color-surface);
background: var(--a2ui-button-background);
box-shadow: none;
color: var(--a2ui-color-on-secondary);
color: var(--a2ui-button-foreground);
font-weight: normal;
transition:
background-color 0.2s ease,
Expand All @@ -25,9 +25,9 @@
}

.button-primary {
background-color: var(--a2ui-color-primary);
background-color: var(--a2ui-button-primary-background);
border: none;
color: var(--a2ui-color-on-primary);
color: var(--a2ui-button-primary-foreground);
}

.button-borderless {
Expand Down
Loading
Loading