Skip to content

Commit fcb23b2

Browse files
bloveclaude
andauthored
refactor(website): one clip player, and give the showcase its aria-labels (#835)
The same video markup had reached three call sites — `page.tsx`, `DemoShowcase.tsx`, and `SolutionDemoBlock.tsx` — identical down to the inline styles. `DemoClip` already gave them a common shape, so the duplication bought nothing. `ClipPlayer` collapses all three, with an `overlay` slot for the one caller that draws a launch button over the video. Making `DemoShowcase` use it required its `DemoMedia` to satisfy `DemoClip`, which surfaced a real gap the production type-check caught: those two clips had no `caption`, so the showcase's `<video>` had been shipping with no accessible name at all while every other clip on the page had one. Rather than invent captions locally, the showcase now spreads `LANGGRAPH_CLIP` and a new `AG_UI_CLIP` from `demo-media.ts`. That also makes an earlier comment true. `LANGGRAPH_CLIP` claimed to exist so the showcase and the switcher could not drift, but only the switcher consumed it; the comment had to be corrected to admit the showcase still hardcoded its own copies. Both now read from one place, so the original claim holds and the caveat is gone. Built homepage unchanged in shape — 5 tablists, 5 `<video>` (one active pane per widget), 0 iframes — and every mounted video now carries its caption as an `aria-label`. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 90be2eb commit fcb23b2

5 files changed

Lines changed: 86 additions & 60 deletions

File tree

apps/website/src/app/page.tsx

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { EcosystemStrip } from '../components/landing/EcosystemStrip';
33
import { Differentiator } from '../components/landing/Differentiator';
44
import { FeatureBlock } from '../components/landing/FeatureBlock';
55
import { BrowserFrame } from '../components/ui/BrowserFrame';
6+
import { ClipPlayer } from '../components/ui/ClipPlayer';
67
import { DemoShowcase } from '../components/landing/DemoShowcase';
78
import { MediumSwitcher } from '../components/landing/MediumSwitcher';
89
import type { MediumPane } from '../components/landing/MediumSwitcher';
@@ -45,24 +46,7 @@ async function buildPanes(media: SectionMedia, clipUrl: string): Promise<MediumP
4546
id: 'video',
4647
key: 'video',
4748
label: 'Video',
48-
content: (
49-
<BrowserFrame url={clipUrl} elevation="lg">
50-
<div style={{ position: 'relative', width: '100%', aspectRatio: '16 / 10', background: '#15161f' }}>
51-
<video
52-
autoPlay
53-
muted
54-
loop
55-
playsInline
56-
poster={clip.poster}
57-
aria-label={clip.caption}
58-
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
59-
>
60-
<source src={clip.videoWebm} type="video/webm" />
61-
<source src={clip.videoMp4} type="video/mp4" />
62-
</video>
63-
</div>
64-
</BrowserFrame>
65-
),
49+
content: <ClipPlayer clip={clip} url={clipUrl} />,
6650
});
6751
}
6852

apps/website/src/components/landing/DemoShowcase.tsx

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,28 @@
11
'use client';
22
import { useState } from 'react';
33
import { tokens } from '@threadplane/design-tokens';
4-
import { BrowserFrame } from '../ui/BrowserFrame';
4+
import { ClipPlayer } from '../ui/ClipPlayer';
55
import { TabGroup } from '../ui/TabGroup';
66
import { Button } from '../ui/Button';
77
import { DemoCtaPair } from './DemoCtaPair';
88
import { DemoModal } from './DemoModal';
99
import { trackCtaClick } from '../../lib/analytics/client';
1010
import { DEMOS } from '../../lib/demos';
11-
import { DEMO_CDN } from '../../lib/demo-media';
11+
import { AG_UI_CLIP, LANGGRAPH_CLIP, type DemoClip } from '../../lib/demo-media';
1212

1313
type TabKey = (typeof DEMOS)[number]['key'];
1414

15-
interface DemoMedia {
15+
/** A runtime tab: the shared clip plus how this section labels and links it. */
16+
interface DemoMedia extends DemoClip {
1617
key: TabKey;
1718
tabLabel: string;
18-
url: string;
19-
videoMp4: string;
20-
videoWebm: string;
21-
poster: string;
2219
href: string;
2320
}
2421

2522

2623
const MEDIA: DemoMedia[] = [
27-
{ key: 'langgraph', tabLabel: 'LangGraph', url: 'demo.threadplane.ai', videoMp4: `${DEMO_CDN}/langgraph-demo.mp4`, videoWebm: `${DEMO_CDN}/langgraph-demo.webm`, poster: `${DEMO_CDN}/langgraph-demo-poster.webp`, href: DEMOS.find((d) => d.key === 'langgraph')!.href },
28-
{ key: 'ag-ui', tabLabel: 'AG-UI', url: 'ag-ui.threadplane.ai', videoMp4: `${DEMO_CDN}/ag-ui-demo.mp4`, videoWebm: `${DEMO_CDN}/ag-ui-demo.webm`, poster: `${DEMO_CDN}/ag-ui-demo-poster.webp`, href: DEMOS.find((d) => d.key === 'ag-ui')!.href },
24+
{ ...LANGGRAPH_CLIP, key: 'langgraph', tabLabel: 'LangGraph', href: DEMOS.find((d) => d.key === 'langgraph')!.href },
25+
{ ...AG_UI_CLIP, key: 'ag-ui', tabLabel: 'AG-UI', href: DEMOS.find((d) => d.key === 'ag-ui')!.href },
2926
];
3027

3128
export function DemoShowcase() {
@@ -61,21 +58,17 @@ export function DemoShowcase() {
6158
id: m.key,
6259
label: m.tabLabel,
6360
content: (
64-
<BrowserFrame url={m.url} elevation="lg">
65-
<div style={{ position: 'relative', width: '100%', aspectRatio: '16 / 10', background: '#15161f' }}>
66-
<video autoPlay muted loop playsInline poster={m.poster}
67-
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}>
68-
<source src={m.videoWebm} type="video/webm" />
69-
<source src={m.videoMp4} type="video/mp4" />
70-
</video>
61+
<ClipPlayer
62+
clip={m}
63+
overlay={
7164
<button onClick={() => launch(m)} aria-label={`Launch ${m.tabLabel} live demo`}
7265
style={{ position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 10,
7366
background: 'linear-gradient(180deg, rgba(16,18,32,.15), rgba(16,18,32,.45))', border: 'none', cursor: 'pointer' }}>
7467
<span style={{ width: 56, height: 56, borderRadius: '50%', background: 'rgba(255,255,255,.95)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#15161f', fontSize: 22 }}>&#9654;</span>
7568
<span style={{ fontFamily: 'Inter, sans-serif', fontWeight: 600, fontSize: 13, color: '#fff', background: 'rgba(0,0,0,.5)', padding: '8px 14px', borderRadius: 8 }}>Launch live demo</span>
7669
</button>
77-
</div>
78-
</BrowserFrame>
70+
}
71+
/>
7972
),
8073
}))}
8174
onSelect={(pane) => setActive(pane.id as TabKey)}

apps/website/src/components/solutions/SolutionDemoBlock.tsx

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { tokens } from '@threadplane/design-tokens';
33
import { Container } from '../ui/Container';
44
import { Section } from '../ui/Section';
55
import { Eyebrow } from '../ui/Eyebrow';
6-
import { BrowserFrame } from '../ui/BrowserFrame';
6+
import { ClipPlayer } from '../ui/ClipPlayer';
77
import type { DemoClip } from '../../lib/demo-media';
88

99
/**
@@ -53,27 +53,7 @@ export function SolutionDemoBlock({ clip, accent }: { clip: DemoClip; accent: st
5353
{clip.caption}
5454
</p>
5555

56-
<BrowserFrame url={clip.url} elevation="lg">
57-
<div style={{ position: 'relative', width: '100%', aspectRatio: '16 / 10', background: '#15161f' }}>
58-
{/*
59-
Silent, decorative loop. `aria-label` rather than captions: there
60-
is no audio track and no narration to caption, and the prose
61-
above already states what the clip shows.
62-
*/}
63-
<video
64-
autoPlay
65-
muted
66-
loop
67-
playsInline
68-
poster={clip.poster}
69-
aria-label={clip.caption}
70-
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
71-
>
72-
<source src={clip.videoWebm} type="video/webm" />
73-
<source src={clip.videoMp4} type="video/mp4" />
74-
</video>
75-
</div>
76-
</BrowserFrame>
56+
<ClipPlayer clip={clip} />
7757

7858
<p
7959
style={{
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// SPDX-License-Identifier: MIT
2+
import type { ReactNode } from 'react';
3+
import { BrowserFrame } from './BrowserFrame';
4+
import type { DemoClip } from '../../lib/demo-media';
5+
6+
interface ClipPlayerProps {
7+
clip: DemoClip;
8+
/**
9+
* Overlay drawn on top of the video — the homepage showcase puts its
10+
* "Launch live demo" button here. Absent everywhere else.
11+
*/
12+
overlay?: ReactNode;
13+
/** Address-bar text, when it should differ from the clip's own. */
14+
url?: string;
15+
}
16+
17+
/**
18+
* A recorded clip in a browser frame.
19+
*
20+
* Extracted after the same markup reached three call sites — the homepage
21+
* sections, the homepage demo showcase, and the solutions pages — identical
22+
* down to the inline styles. `DemoClip` already gave them a common shape, so
23+
* the duplication bought nothing.
24+
*
25+
* Silent and decorative: no audio track, no narration, so `aria-label` carries
26+
* the description and there is nothing for captions to caption. Callers mount
27+
* this only when its pane is active, which is what keeps a page with several
28+
* clips from fetching all of them at once.
29+
*/
30+
export function ClipPlayer({ clip, overlay, url }: ClipPlayerProps) {
31+
return (
32+
<BrowserFrame url={url ?? clip.url} elevation="lg">
33+
<div
34+
style={{
35+
position: 'relative',
36+
width: '100%',
37+
aspectRatio: '16 / 10',
38+
background: '#15161f',
39+
}}
40+
>
41+
<video
42+
autoPlay
43+
muted
44+
loop
45+
playsInline
46+
poster={clip.poster}
47+
aria-label={clip.caption}
48+
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
49+
>
50+
<source src={clip.videoWebm} type="video/webm" />
51+
<source src={clip.videoMp4} type="video/mp4" />
52+
</video>
53+
{overlay}
54+
</div>
55+
</BrowserFrame>
56+
);
57+
}

apps/website/src/lib/demo-media.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ export const HITL_CLIP: DemoClip = {
4141

4242
/**
4343
* The LangGraph streaming demo, recorded on the canonical demo shell. Exported
44-
* so the section switcher does not add another hardcoded copy of these URLs.
45-
* `DemoShowcase` still declares its own; consolidating it is tracked separately.
44+
* Shared by the homepage showcase and the section switcher, so a recut or a
45+
* store move changes one place rather than three.
4646
*/
4747
export const LANGGRAPH_CLIP: DemoClip = {
4848
caption: 'Tokens stream into the Angular surface as the agent produces them.',
@@ -52,6 +52,18 @@ export const LANGGRAPH_CLIP: DemoClip = {
5252
poster: `${DEMO_CDN}/langgraph-demo-poster.webp`,
5353
};
5454

55+
/**
56+
* The AG-UI runtime demo. Paired with `LANGGRAPH_CLIP` so the homepage showcase
57+
* can state "same front end, two runtimes" from one place.
58+
*/
59+
export const AG_UI_CLIP: DemoClip = {
60+
caption: 'The same Threadplane chat surface, running against an AG-UI backend.',
61+
url: 'ag-ui.threadplane.ai',
62+
videoMp4: `${DEMO_CDN}/ag-ui-demo.mp4`,
63+
videoWebm: `${DEMO_CDN}/ag-ui-demo.webm`,
64+
poster: `${DEMO_CDN}/ag-ui-demo-poster.webp`,
65+
};
66+
5567
/**
5668
* Generative UI: the agent emits a json-render spec and the demo mounts it as
5769
* real Angular components. Recorded by

0 commit comments

Comments
 (0)