From 0d1d10e9f88ff701906fede2bc1d1fea2f28d16a Mon Sep 17 00:00:00 2001 From: "xuan.huang" <5563315+Huxpro@users.noreply.github.com> Date: Mon, 4 May 2026 01:41:56 +0200 Subject: [PATCH 1/5] feat(a2ui-playground): add fullscreen preview for mobile - Add expand button in preview panel header to toggle fullscreen - Fullscreen overlay covers entire viewport with close button - Auto-enter fullscreen on mobile (<=980px) when rendering starts - Improves mobile UX where preview panel had limited height --- .../a2ui-playground/src/pages/DemosPage.tsx | 32 ++++++++++++------- packages/genui/a2ui-playground/src/styles.css | 31 ++++++++++++++++++ 2 files changed, 51 insertions(+), 12 deletions(-) diff --git a/packages/genui/a2ui-playground/src/pages/DemosPage.tsx b/packages/genui/a2ui-playground/src/pages/DemosPage.tsx index 9cb265e1e6..0f82c2a5c4 100644 --- a/packages/genui/a2ui-playground/src/pages/DemosPage.tsx +++ b/packages/genui/a2ui-playground/src/pages/DemosPage.tsx @@ -4,8 +4,6 @@ import { json } from '@codemirror/lang-json'; import CodeMirror from '@uiw/react-codemirror'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; - -import { Chip } from '../components/Chip.js'; import { MobilePreview } from '../components/MobilePreview.js'; import { QrCode } from '../components/QrCode.js'; import { DYNAMIC_PRESETS, STATIC_DEMOS } from '../demos.js'; @@ -95,6 +93,7 @@ export function DemosPage(props: { protocol: ProtocolVersion }) { const [showSimTooltip, setShowSimTooltip] = useState(false); const [jsonEdited, setJsonEdited] = useState(false); const [previewMode, setPreviewMode] = useState<'phone' | 'full'>('phone'); + const [fullscreen, setFullscreen] = useState(false); const baseUrl = window.location.href.replace(/#.*$/, ''); const rspeedyDevUrl = useRspeedyDevUrl(); @@ -151,6 +150,11 @@ export function DemosPage(props: { protocol: ProtocolVersion }) { ); setRenderUrl(url); + // On mobile, auto-expand preview to fullscreen when rendering. + if (window.innerWidth <= 980) { + setFullscreen(true); + } + // Native in-app preview: pass A2UI payload via global props, directly through URL query. // In Lynx, query params are exposed in `lynx.__globalProps` / `useGlobalProps()`. const seq = ++lynxUrlSeqRef.current; @@ -363,18 +367,14 @@ export function DemosPage(props: { protocol: ProtocolVersion }) { {/* Preview Panel */} -
+
Lynx Preview - {currentScenario - ? ( -
-
- {currentScenario.tags.map((t) => {t})} -
-
- ) - : null} +
+
{isSimulated ? ( diff --git a/packages/genui/a2ui-playground/src/styles.css b/packages/genui/a2ui-playground/src/styles.css index 3eadc57ac2..af2a3c9e86 100644 --- a/packages/genui/a2ui-playground/src/styles.css +++ b/packages/genui/a2ui-playground/src/styles.css @@ -299,6 +299,37 @@ a { border-left: 1px solid var(--geist-border); } +.previewPanelFullscreen { + position: fixed; + inset: 0; + z-index: 200; + width: 100%; + border-left: none; + background: var(--geist-background); +} + +.previewExpandBtn { + width: 28px; + height: 28px; + border: none; + border-radius: var(--geist-radius-sm); + background: transparent; + color: var(--geist-secondary); + font-size: 16px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all var(--geist-transition); + flex-shrink: 0; + padding: 0; +} + +.previewExpandBtn:hover { + color: var(--geist-foreground); + background: var(--geist-surface); +} + .previewPanelHeader { display: flex; align-items: center; From e0794cfa91ae3618abb1c121737563cadd2d1f94 Mon Sep 17 00:00:00 2001 From: "xuan.huang" <5563315+Huxpro@users.noreply.github.com> Date: Mon, 4 May 2026 01:43:07 +0200 Subject: [PATCH 2/5] feat(a2ui-playground): default to full preview mode on mobile --- packages/genui/a2ui-playground/src/pages/DemosPage.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/genui/a2ui-playground/src/pages/DemosPage.tsx b/packages/genui/a2ui-playground/src/pages/DemosPage.tsx index 0f82c2a5c4..4fb26fd4c4 100644 --- a/packages/genui/a2ui-playground/src/pages/DemosPage.tsx +++ b/packages/genui/a2ui-playground/src/pages/DemosPage.tsx @@ -4,6 +4,7 @@ import { json } from '@codemirror/lang-json'; import CodeMirror from '@uiw/react-codemirror'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; + import { MobilePreview } from '../components/MobilePreview.js'; import { QrCode } from '../components/QrCode.js'; import { DYNAMIC_PRESETS, STATIC_DEMOS } from '../demos.js'; @@ -92,7 +93,9 @@ export function DemosPage(props: { protocol: ProtocolVersion }) { const [speed, setSpeed] = useState(1); const [showSimTooltip, setShowSimTooltip] = useState(false); const [jsonEdited, setJsonEdited] = useState(false); - const [previewMode, setPreviewMode] = useState<'phone' | 'full'>('phone'); + const [previewMode, setPreviewMode] = useState<'phone' | 'full'>( + () => window.innerWidth <= 980 ? 'full' : 'phone', + ); const [fullscreen, setFullscreen] = useState(false); const baseUrl = window.location.href.replace(/#.*$/, ''); From 7f438800ee00de7f708ad3b2e19b11d33bf67f2a Mon Sep 17 00:00:00 2001 From: "xuan.huang" <5563315+Huxpro@users.noreply.github.com> Date: Mon, 4 May 2026 01:45:04 +0200 Subject: [PATCH 3/5] feat(a2ui-playground): show component tags in sidebar scenarios Move the component chips (Card, Column, Image, etc.) from the preview panel header into the sidebar under each scenario name, so users can see which A2UI components each demo showcases before selecting it. --- .../a2ui-playground/src/pages/DemosPage.tsx | 5 +++++ packages/genui/a2ui-playground/src/styles.css | 17 +++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/packages/genui/a2ui-playground/src/pages/DemosPage.tsx b/packages/genui/a2ui-playground/src/pages/DemosPage.tsx index 4fb26fd4c4..8c676bff50 100644 --- a/packages/genui/a2ui-playground/src/pages/DemosPage.tsx +++ b/packages/genui/a2ui-playground/src/pages/DemosPage.tsx @@ -309,6 +309,11 @@ export function DemosPage(props: { protocol: ProtocolVersion }) { onClick={() => handleSelectScenario(s.id)} > {s.title} +
+ {s.tags.map((t) => ( + {t} + ))} +
))}
diff --git a/packages/genui/a2ui-playground/src/styles.css b/packages/genui/a2ui-playground/src/styles.css index af2a3c9e86..d84d251a57 100644 --- a/packages/genui/a2ui-playground/src/styles.css +++ b/packages/genui/a2ui-playground/src/styles.css @@ -696,6 +696,23 @@ a { color: var(--geist-foreground); } +.scenarioTags { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-top: 4px; +} + +.scenarioTag { + font-size: 10px; + font-weight: 500; + color: var(--geist-secondary); + padding: 1px 6px; + border-radius: 4px; + background: var(--geist-surface); + border: 1px solid var(--geist-border); +} + .scenarioDesc { font-size: 11px; color: var(--geist-secondary); From 15c089ae8432456dd6f3ce15ed39ff8a14ca846a Mon Sep 17 00:00:00 2001 From: "xuan.huang" <5563315+Huxpro@users.noreply.github.com> Date: Mon, 4 May 2026 01:50:18 +0200 Subject: [PATCH 4/5] feat(a2ui-playground): live component stack during streaming Show a live "Components" bar at the bottom of the preview panel that reveals component names (Card, Column, Image, etc.) as they appear during streaming simulation. Each tag animates in, synced with the replay speed. Uses componentsByMessage() to extract per-message component introductions from the demo JSON data. --- packages/genui/a2ui-playground/src/demos.ts | 22 ++++++++ .../a2ui-playground/src/pages/DemosPage.tsx | 39 +++++++++++++- packages/genui/a2ui-playground/src/styles.css | 51 +++++++++++++++++++ 3 files changed, 111 insertions(+), 1 deletion(-) diff --git a/packages/genui/a2ui-playground/src/demos.ts b/packages/genui/a2ui-playground/src/demos.ts index 9bde9966c9..4859a14796 100644 --- a/packages/genui/a2ui-playground/src/demos.ts +++ b/packages/genui/a2ui-playground/src/demos.ts @@ -53,6 +53,28 @@ function tagsFromMessages(messages: unknown): string[] { return Array.from(out).sort((a, b) => a.localeCompare(b)); } +/** + * Extract new component names introduced by each message (in order). + * Returns an array parallel to the messages array: each entry is + * the list of component names that appear for the first time in that message. + */ +export function componentsByMessage(messages: unknown): string[][] { + if (!Array.isArray(messages)) return []; + const seen = new Set(); + return messages.map((msg) => { + const msgComponents = new Set(); + collectComponentNamesFromMessages(msg, msgComponents); + const newOnes: string[] = []; + for (const name of msgComponents) { + if (!seen.has(name)) { + seen.add(name); + newOnes.push(name); + } + } + return newOnes; + }); +} + export interface StaticDemo { id: string; title: string; diff --git a/packages/genui/a2ui-playground/src/pages/DemosPage.tsx b/packages/genui/a2ui-playground/src/pages/DemosPage.tsx index 8c676bff50..f2b68f4b4e 100644 --- a/packages/genui/a2ui-playground/src/pages/DemosPage.tsx +++ b/packages/genui/a2ui-playground/src/pages/DemosPage.tsx @@ -7,7 +7,11 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { MobilePreview } from '../components/MobilePreview.js'; import { QrCode } from '../components/QrCode.js'; -import { DYNAMIC_PRESETS, STATIC_DEMOS } from '../demos.js'; +import { + DYNAMIC_PRESETS, + STATIC_DEMOS, + componentsByMessage, +} from '../demos.js'; import { DEFAULT_DEMO_URL } from '../utils/demoUrl.js'; import type { ProtocolVersion } from '../utils/protocol.js'; import { buildRenderUrl } from '../utils/renderUrl.js'; @@ -97,6 +101,8 @@ export function DemosPage(props: { protocol: ProtocolVersion }) { () => window.innerWidth <= 980 ? 'full' : 'phone', ); const [fullscreen, setFullscreen] = useState(false); + const [liveComponents, setLiveComponents] = useState([]); + const liveTimersRef = useRef[]>([]); const baseUrl = window.location.href.replace(/#.*$/, ''); const rspeedyDevUrl = useRspeedyDevUrl(); @@ -153,6 +159,23 @@ export function DemosPage(props: { protocol: ProtocolVersion }) { ); setRenderUrl(url); + // Live component stack: reveal component names as they would appear + // during streaming, synced with the replay speed. + for (const t of liveTimersRef.current) clearTimeout(t); + liveTimersRef.current = []; + setLiveComponents([]); + const perMsg = componentsByMessage(parsed); + const delayMs = 800 / (speed || 1); + let accumulated: string[] = []; + perMsg.forEach((newNames, i) => { + if (newNames.length === 0) return; + const timer = setTimeout(() => { + accumulated = [...accumulated, ...newNames]; + setLiveComponents([...accumulated]); + }, delayMs * (i + 1)); + liveTimersRef.current.push(timer); + }); + // On mobile, auto-expand preview to fullscreen when rendering. if (window.innerWidth <= 980) { setFullscreen(true); @@ -483,6 +506,20 @@ export function DemosPage(props: { protocol: ProtocolVersion }) { )}
+ {/* Live Component Stack */} + {liveComponents.length > 0 + ? ( +
+ Components +
+ {liveComponents.map((name) => ( + {name} + ))} +
+
+ ) + : null} + {/* QR Code Section — only shown when there's a render URL */} {renderUrl || lynxDevUrl ? ( diff --git a/packages/genui/a2ui-playground/src/styles.css b/packages/genui/a2ui-playground/src/styles.css index d84d251a57..c352e74b47 100644 --- a/packages/genui/a2ui-playground/src/styles.css +++ b/packages/genui/a2ui-playground/src/styles.css @@ -390,6 +390,57 @@ a { padding: 0; } +/* ── Live Component Stack ── */ +.liveComponentStack { + flex-shrink: 0; + display: flex; + align-items: center; + gap: 8px; + padding: 6px 16px; + border-top: 1px solid var(--geist-border); + background: var(--geist-background); + font-size: 12px; + overflow-x: auto; +} + +.liveComponentLabel { + font-weight: 600; + color: var(--geist-secondary); + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.04em; + flex-shrink: 0; +} + +.liveComponentTags { + display: flex; + gap: 4px; + flex-wrap: nowrap; +} + +.liveComponentTag { + padding: 2px 8px; + border-radius: 4px; + background: var(--geist-surface); + border: 1px solid var(--geist-border); + font-size: 11px; + font-weight: 500; + color: var(--geist-foreground); + white-space: nowrap; + animation: tagAppear 300ms ease-out; +} + +@keyframes tagAppear { + from { + opacity: 0; + transform: translateY(4px) scale(0.9); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + .previewFullIframe { width: 100%; height: 100%; From 4748494ba3fbca5f0f32b519dfcc70aa509a3392 Mon Sep 17 00:00:00 2001 From: "xuan.huang" <5563315+Huxpro@users.noreply.github.com> Date: Mon, 4 May 2026 01:51:28 +0200 Subject: [PATCH 5/5] refactor(a2ui-playground): remove static component tags from sidebar scenarios --- packages/genui/a2ui-playground/src/pages/DemosPage.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/genui/a2ui-playground/src/pages/DemosPage.tsx b/packages/genui/a2ui-playground/src/pages/DemosPage.tsx index f2b68f4b4e..e0451f6629 100644 --- a/packages/genui/a2ui-playground/src/pages/DemosPage.tsx +++ b/packages/genui/a2ui-playground/src/pages/DemosPage.tsx @@ -332,11 +332,6 @@ export function DemosPage(props: { protocol: ProtocolVersion }) { onClick={() => handleSelectScenario(s.id)} > {s.title} -
- {s.tags.map((t) => ( - {t} - ))} -
))}