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
69 changes: 69 additions & 0 deletions packages/genui/a2ui-playground/src/components/MobileTabBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// 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 type MobilePaneTab = 'edit' | 'preview';

interface MobileTabBarProps {
activeTab: MobilePaneTab;
onChange: (tab: MobilePaneTab) => void;
editLabel?: string;
}

export function MobileTabBar(props: MobileTabBarProps) {
const { activeTab, editLabel = 'Edit', onChange } = props;
return (
<nav
className='mobileTabBar'
role='tablist'
aria-label='Active panel'
>
<button
type='button'
role='tab'
aria-selected={activeTab === 'edit'}
className={activeTab === 'edit' ? 'mobileTab active' : 'mobileTab'}
onClick={() => onChange('edit')}
>
<svg
viewBox='0 0 24 24'
width='18'
height='18'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
aria-hidden='true'
>
<path d='M12 20h9' />
<path d='M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z' />
</svg>
<span className='mobileTabLabel'>{editLabel}</span>
</button>
<button
type='button'
role='tab'
aria-selected={activeTab === 'preview'}
className={activeTab === 'preview' ? 'mobileTab active' : 'mobileTab'}
onClick={() => onChange('preview')}
>
<svg
viewBox='0 0 24 24'
width='18'
height='18'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
aria-hidden='true'
>
<rect x='6' y='2' width='12' height='20' rx='2.5' ry='2.5' />
<line x1='12' y1='18' x2='12.01' y2='18' />
</svg>
<span className='mobileTabLabel'>Preview</span>
</button>
</nav>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -592,7 +592,15 @@ export function PreviewPanel(props: PreviewPanelProps) {
return;
}

if (typeof window !== 'undefined' && window.innerWidth <= 980) {
// Auto-fullscreen on narrow but tab-less screens (721–980px). At ≤720
// the host page renders a MobileTabBar, and the Preview tab already
// gives the panel the full viewport — auto-fullscreening on top of
// that hides the tab bar and traps the user behind an X button.
if (
typeof window !== 'undefined'
&& window.innerWidth > 720
&& window.innerWidth <= 980
) {
setIsFullscreen(true);
}
}, [previewSource]);
Expand Down
114 changes: 105 additions & 9 deletions packages/genui/a2ui-playground/src/pages/AIChatPage.css
Original file line number Diff line number Diff line change
Expand Up @@ -1435,22 +1435,118 @@
max-width: 100%;
}

.chatComposerFooter {
align-items: stretch;
flex-direction: column;
/* Tab-mode: each tab is full screen, no inline split / resize handle.
Active pane is sized via .chatPage[data-active-tab='…'] rules below. */
.chatPage[data-active-tab="edit"] .previewPanel {
display: none;
}
.chatPage[data-active-tab="preview"] .chatPanel,
.chatPage[data-active-tab="preview"] .conversationPanel {
display: none;
}
.chatPageBody > .panelResizeHandle {
display: none;
}

.chatProviderControl {
max-width: none;
/* The active pane fills the body — overrides the desktop sizing baked
into .conversationPanel / .chatPanel / .previewPanel. */
.chatPage[data-active-tab="edit"] .chatPanel,
.chatPage[data-active-tab="preview"] .previewPanel {
flex: 1;
width: 100%;
max-width: none;
min-height: 0;
}
.chatPage[data-active-tab="preview"] .previewPanel {
border-top: none;
border-left: none;
}

.chatProviderSelect {
max-width: none;
width: 100%;
/* Conversation panel collapses into a single horizontal strip on
phones: +New Chat is an icon button on the left, conversations
scroll as compact one-line cards on the right. Drops from ~150px
to ~52px of vertical chrome. */
.chatPage[data-active-tab="edit"] .conversationPanel {
flex-direction: row;
align-items: stretch;
min-height: 0;
max-height: none;
height: 52px;
background: var(--geist-background);
}
.conversationPanelHeader,
.conversationPanelCreateRow {
flex: 0 0 auto;
width: auto;
min-height: 0;
padding: 8px;
border-bottom: none;
border-right: 1px solid var(--geist-border);
background: transparent;
}
.conversationNewButton {
width: 36px;
height: 36px;
padding: 0;
border-radius: 999px;
box-shadow: none;
}
.conversationNewButtonLabel {
display: none;
}
.conversationList {
flex: 1;
align-items: center;
padding: 6px 8px;
gap: 6px;
}
/* Keep the desktop card aesthetic on mobile: rounded rectangle, active
bar on the left, Edit/Del hidden by default and revealed only on the
active card (matching the desktop hover/active behavior). Vertically
center title + actions since the meta/date row is hidden — the
desktop card is a two-line block, the mobile chip is single-line. */
.conversationListItem {
flex: 0 0 auto;
width: auto;
min-width: 0;
max-width: 220px;
padding: 6px 8px 6px 12px;
align-items: center;
}
.conversationListItemMeta,
.conversationListItemPreview {
display: none;
}
.conversationListItemTitle {
font-size: 12px;
line-height: 1.25;
}
.conversationListItemActions {
opacity: 0;
pointer-events: none;
transform: none;
gap: 4px;
}
.conversationListItem-active .conversationListItemActions,
.conversationListItem:focus-within .conversationListItemActions {
opacity: 1;
pointer-events: auto;
}

/* Pill Send button (drops the desktop drop-shadow on mobile). */
.chatSendBtn {
width: 100%;
min-width: 84px;
height: 38px;
padding: 0 14px;
font-size: 13px;
box-shadow: none;
}

/* Drop the "Create / Online Agent / Describe the UI…" header on
mobile — the chat input area itself communicates the same thing
and this band was eating ~120px of vertical space. (Token-usage
badge lives here too, only relevant for power users on desktop.) */
.chatHeader {
display: none;
}
}
12 changes: 12 additions & 0 deletions packages/genui/a2ui-playground/src/pages/AIChatPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { ConfirmDialog } from '../components/ConfirmDialog.js';
import { ConversationListPanel } from '../components/ConversationListPanel.js';
import { CopyToast, useCopyToast } from '../components/CopyToast.js';
import { InstantExamplesStrip } from '../components/InstantExamplesStrip.js';
import { MobileTabBar } from '../components/MobileTabBar.js';
import type { MobilePaneTab } from '../components/MobileTabBar.js';
import { PageHeader } from '../components/PageHeader.js';
import { PanelResizeHandle } from '../components/PanelResizeHandle.js';
import { PreviewPanel } from '../components/PreviewPanel.js';
Expand Down Expand Up @@ -854,6 +856,9 @@ export function AIChatPage(
PreviewPayloadUrls | null
>(null);
const [isGenerating, setIsGenerating] = useState<boolean>(false);
const [activeMobileTab, setActiveMobileTab] = useState<MobilePaneTab>(
'edit',
);
const [deleteConversationId, setDeleteConversationId] = useState<
string | null
>(null);
Expand Down Expand Up @@ -1665,6 +1670,7 @@ export function AIChatPage(
<div
ref={pageRef}
className={isPanelResizing ? 'chatPage resizing' : 'chatPage'}
data-active-tab={activeMobileTab}
>
<CopyToast toast={copyToast} />
<ConfirmDialog
Expand Down Expand Up @@ -1970,6 +1976,12 @@ export function AIChatPage(
/>
</PreviewPanel>
</div>

<MobileTabBar
activeTab={activeMobileTab}
onChange={setActiveMobileTab}
editLabel='Chat'
/>
</div>
);
}
113 changes: 113 additions & 0 deletions packages/genui/a2ui-playground/src/pages/DemosPage.css
Original file line number Diff line number Diff line change
Expand Up @@ -1059,3 +1059,116 @@ html[data-theme="dark"] .detailBackButton:hover {
border-top: 1px solid var(--geist-border);
}
}

/* Phone-sized tab mode: each tab is full screen, no inline split. */
@media (max-width: 720px) {
.demosPage[data-active-tab="edit"] .examplesPreviewWrap {
display: none;
}
.demosPage[data-active-tab="preview"] .sidebar,
.demosPage[data-active-tab="preview"] .codePanel {
display: none;
}
.demosPage > .panelResizeHandle {
display: none;
}

/* Preview pane fills the viewport (minus top nav + bottom tab bar);
overrides the desktop .examplesPreviewWrap min-height: 480px. */
.demosPage[data-active-tab="preview"] .examplesPreviewWrap {
flex: 1;
width: 100%;
min-height: 0;
}

/* Sidebar collapses into a single horizontal strip: back-icon on the
left, scenarios scrolling as chips on the right. SCENARIOS heading
hides — its label is redundant when the chips are the only thing in
the strip. Drops the band from ~180px to ~52px. */
.demosPage[data-active-tab="edit"] .sidebar {
flex-direction: row;
align-items: stretch;
width: 100%;
max-height: none;
min-height: 0;
height: 52px;
overflow: hidden;
background: var(--geist-background);
}
/* Code panel grows to fill the remaining viewport instead of staying
pinned at min-height 280px (the desktop / <=980 default). */
.demosPage[data-active-tab="edit"] .codePanel {
flex: 1;
min-height: 0;
}
.sidebarTopNav {
flex: 0 0 auto;
padding: 8px 10px;
border-right: 1px solid var(--geist-border);
}
.detailBackLabel {
display: none;
}
.detailBackButton {
min-height: 32px;
padding: 6px 10px;
box-shadow: none;
}
.sidebarSection {
flex: 1;
min-width: 0;
padding: 0;
display: flex;
align-items: center;
}
.sidebarHeading {
display: none;
}
.scenarioList {
flex-direction: row;
gap: 6px;
padding: 6px 10px;
overflow-x: auto;
overflow-y: hidden;
width: 100%;
}
.scenarioItem {
flex: 0 0 auto;
flex-direction: row;
align-items: center;
width: auto;
min-height: 0;
padding: 6px 12px;
border-radius: 999px;
border-color: var(--geist-border);
}
.scenarioItem .scenarioDesc {
display: none;
}
.scenarioItem .scenarioName {
white-space: nowrap;
font-size: 12px;
}

/* Tighter toolbar so title + Reset/Clear/Render all stay on one row
in the constrained mobile width. */
.codePanelToolbar {
padding: 6px 10px;
}
.codePanelToolbar .toolbarBtn {
height: 26px;
padding: 0 8px;
font-size: 11px;
}
.codePanelToolbar .toolbarBtn.primary {
padding: 0 10px;
}
/* "A2UI Messages" was wrapping to two lines — keep it on one. */
.codePanelTitle {
white-space: nowrap;
font-size: 12px;
}
.codePanelBadge {
display: none;
}
}
Loading
Loading