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
12 changes: 5 additions & 7 deletions app/components/HomeDemos.vue
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
<script setup lang="ts">
import type { HomeDemoItem } from '~/utils/home-demos'
import { HOME_DEMOS_MAX, HOME_DEMOS_MIN } from '~/utils/home-demos'
import { HOME_DEMOS_MIN } from '~/utils/home-demos'
import DemoIndex from './DemoIndex.vue'

/**
* The homepage teaser for demos: the first few, pinned films first, and a
* link to every demo on `/skills/demos`. The page fetches
* `/api/skill-demos` and passes the items.
* `/api/skill-demos` and passes the teaser items and full count.
*/
const { demos: items } = defineProps<{ demos: HomeDemoItem[] }>()
const { demos, total } = defineProps<{ demos: HomeDemoItem[], total: number }>()

const headingId = useId()
const demos = computed(() => items.slice(0, HOME_DEMOS_MAX))
const show = computed(() => demos.value.length >= HOME_DEMOS_MIN)
const more = computed(() => items.length)
const show = computed(() => demos.length >= HOME_DEMOS_MIN)
</script>

<template>
Expand All @@ -31,7 +29,7 @@ const more = computed(() => items.length)

<UButton
to="/skills/demos"
:label="`All ${more} demos`"
:label="`All ${total} demos`"
color="neutral"
variant="ghost"
size="sm"
Expand Down
16 changes: 2 additions & 14 deletions app/components/why/_WhyPreviewsVisual.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<script setup lang="ts">
import type { HomeDemoItem } from '~/utils/home-demos'
import { DEMO_GROUPS } from '#shared/demo-groups'
import { demoHref, demoRecording } from '~/utils/home-demos'
import { demoHref, demoPreviews, demoRecording } from '~/utils/home-demos'
import WhyPanel from './_WhyPanel.vue'

/**
Expand All @@ -16,18 +15,7 @@ const { demos } = defineProps<{
demos: readonly HomeDemoItem[]
}>()

/** Stills in the row. Three compare; more shrink past reading. */
const SHOWN = 3

const PREFERRED_GROUP = 'landing-page'

const picked = computed(() => {
const groups = DEMO_GROUPS
.map(group => ({ group, items: demos.filter(demo => demo.makes === group.makes) }))
.filter(entry => entry.items.length >= 2)
const entry = groups.find(candidate => candidate.group.makes === PREFERRED_GROUP) ?? groups[0]
return entry ? { label: entry.group.label, items: entry.items.slice(0, SHOWN) } : null
})
const picked = computed(() => demoPreviews(demos))

function still(demo: HomeDemoItem) {
if (demo.video)
Expand Down
11 changes: 7 additions & 4 deletions app/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import type { RecentPublishesResponse } from '~~/server/api/feed/recent-publishe
import type { RecentUpdateCard, RecentUpdatesResponse } from '~~/server/api/feed/recent-updates.get'
import type { TrendingFeedResponse } from '~~/server/api/feed/trending.get'
import type { RecentPullRequestsResponse } from '#shared/open-source-pull-requests'
import type { HomeDemoItem } from '../utils/home-demos'
import { agentSetupPrompt } from '#shared/agent-setup'
import { WRITING_COMPARISON_LINK } from '#shared/comparison-navigation'
import { avatarProxyUrl } from '#shared/image-proxy'
import { feedBoardRows, trendingRangeMeta } from '#shared/trending-range'
import OutcomeClusterGrid from '../components/OutcomeClusterGrid.vue'
import { AGENT_LOGOS } from '../utils/agent-logos'
import { MORE_AGENT_COUNT } from '../utils/agent-reach'
import { homeDemoFeed } from '../utils/home-demos'

const setupPrompt = agentSetupPrompt()
const heroAgentsLabel = `Works with ${AGENT_LOGOS.map(agent => agent.label).join(', ')}, and ${MORE_AGENT_COUNT} more Agents`
Expand Down Expand Up @@ -107,7 +107,10 @@ if (import.meta.server) {
}

// Lazy: the section sits under the hero, and its images load lazily anyway.
const { data: demosData } = useLazyFetch<{ items: HomeDemoItem[] }>('/api/skill-demos', { key: 'home-skill-demos' })
const { data: demosData } = useLazyFetch('/api/skill-demos', {
key: 'home-skill-demos-v2',
transform: homeDemoFeed,
})

// Lazy too: the Why band sits below the fold, and the feed is cached for a day.
const { data: pullsData } = useLazyFetch<RecentPullRequestsResponse>('/api/feed/recent-pull-requests', { key: 'home-recent-pull-requests' })
Expand Down Expand Up @@ -288,7 +291,7 @@ function recentRepoDescription(item: RecentRepoUpdate): string {
waits on.
-->
<!-- The first content section: what Skills make, before any list of them. -->
<LazyHomeDemos hydrate-on-visible :demos="demosData?.items ?? []" />
<LazyHomeDemos hydrate-on-visible :demos="demosData?.items ?? []" :total="demosData?.total ?? 0" />

<section
v-if="showTrending || trendingStatus === 'pending'"
Expand Down Expand Up @@ -328,7 +331,7 @@ function recentRepoDescription(item: RecentRepoUpdate): string {
</div>
</section>

<LazyHomeWhy hydrate-on-visible :trending-row="whyTrendingRow" :demos="demosData?.items ?? []" :pulls="pullsData?.items ?? []" />
<LazyHomeWhy hydrate-on-visible :trending-row="whyTrendingRow" :demos="demosData?.previews ?? []" :pulls="pullsData?.items ?? []" />

<LazyHomeAgents hydrate-on-visible />

Expand Down
19 changes: 19 additions & 0 deletions app/utils/home-demos.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { DemoMakes } from '#shared/demo-groups'
import type { SkillCardSkill } from '~/types/skill-card'
import { DEMO_GROUPS } from '#shared/demo-groups'
import { demoAgentIcon, demoModelLabel } from '#shared/demo-recording'

/**
Expand Down Expand Up @@ -51,6 +52,24 @@ export interface HomeDemoShot {
export const HOME_DEMOS_MIN = 3
export const HOME_DEMOS_MAX = 6

/** Compare up to three recordings in one group. Landing pages lead when available. */
export function demoPreviews(demos: readonly HomeDemoItem[]) {
const groups = DEMO_GROUPS
.map(group => ({ group, items: demos.filter(demo => demo.makes === group.makes) }))
.filter(entry => entry.items.length >= 2)
const entry = groups.find(candidate => candidate.group.makes === 'landing-page') ?? groups[0]
return entry ? { label: entry.group.label, items: entry.items.slice(0, 3) } : null
}

/** Keep only rendered demos in the homepage payload, plus the full feed count. */
export function homeDemoFeed(feed: { items: HomeDemoItem[] }) {
return {
items: feed.items.slice(0, HOME_DEMOS_MAX),
previews: demoPreviews(feed.items)?.items ?? [],
total: feed.items.length,
}
}

/**
* Below this width a demo shows its phone shot, when it has one. Stylesheets
* repeat the value as `max-width: 39.99rem`, the `sm` breakpoint.
Expand Down
6 changes: 3 additions & 3 deletions test/a11y.nuxt.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -637,7 +637,7 @@ describe('accessibility: components', () => {
const container = createIsolatedContainer()
const wrapper = await mountSuspended(
await loadComponent('HomeDemos'),
{ attachTo: container, props: { demos } },
{ attachTo: container, props: { demos, total: 42 } },
)
const results = await runAxe(container)
expect(results.violations, formatViolations(results)).toHaveLength(0)
Expand All @@ -652,7 +652,7 @@ describe('accessibility: components', () => {
await new Promise(done => setTimeout(done, 400))
expect(picks[1]?.getAttribute('aria-pressed')).toBe('true')
expect(stage()).toBe('/skills/demos/anthropics/skills/two')
expect(container.querySelector('a[href="/skills/demos"]')?.textContent).toContain('All 3 demos')
expect(container.querySelector('a[href="/skills/demos"]')?.textContent).toContain('All 42 demos')
wrapper.unmount()
})

Expand Down Expand Up @@ -694,7 +694,7 @@ describe('accessibility: components', () => {
const container = createIsolatedContainer()
const wrapper = await mountSuspended(
await loadComponent('HomeDemos'),
{ attachTo: container, props: { demos: [] } },
{ attachTo: container, props: { demos: [], total: 0 } },
)
expect(container.querySelector('#demos')).toBeNull()
wrapper.unmount()
Expand Down
30 changes: 29 additions & 1 deletion test/unit/home-demos.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { HomeDemoItem } from '../../app/utils/home-demos'
import { describe, expect, it } from 'vitest'
import { demoPhoneRatio, demoRecording, demoSocialPicture, formatDemoDuration } from '../../app/utils/home-demos'
import { demoPhoneRatio, demoRecording, demoSocialPicture, formatDemoDuration, homeDemoFeed } from '../../app/utils/home-demos'

function demo(overrides: Partial<HomeDemoItem> = {}): HomeDemoItem {
return {
Expand All @@ -15,6 +15,8 @@ function demo(overrides: Partial<HomeDemoItem> = {}): HomeDemoItem {
agent: 'Claude Code',
model: 'claude-opus-5-5',
recordedAt: '2026-10-06',
liveUrl: null,
video: null,
shots: [
{ src: '/d.jpg', width: 1440, height: 900, alt: 'Desktop', viewport: 'desktop', poster: null },
{ src: '/m.jpg', width: 390, height: 844, alt: 'Phone', viewport: 'mobile', poster: null },
Expand All @@ -23,6 +25,32 @@ function demo(overrides: Partial<HomeDemoItem> = {}): HomeDemoItem {
}
}

describe('homeDemoFeed', () => {
it('keeps the teaser order, total count, and preferred comparison beyond the teaser', () => {
const films = Array.from({ length: 7 }, (_, index) => demo({ name: `film-${index}`, makes: 'film' }))
const pages = Array.from({ length: 4 }, (_, index) => demo({ name: `page-${index}`, makes: 'landing-page' }))
const result = homeDemoFeed({ items: [...films, ...pages] })
expect(result).toEqual({ items: films.slice(0, 6), previews: pages.slice(0, 3), total: 11 })
})

it('falls back to the first eligible group in display order', () => {
const component = demo({ name: 'component' })
const page = demo({ name: 'page', makes: 'landing-page' })
const films = [demo({ name: 'film-a', makes: 'film' }), demo({ name: 'film-b', makes: 'film' })]
const items = [component, page, ...films, demo({ name: 'other-component' })]
expect(homeDemoFeed({ items })).toEqual({ items, previews: films, total: 5 })
})

it('leaves comparison empty when no group has two recordings', () => {
const items = [demo(), demo({ makes: 'landing-page' })]
expect(homeDemoFeed({ items })).toEqual({ items, previews: [], total: 2 })
})

it('keeps an empty feed empty', () => {
expect(homeDemoFeed({ items: [] })).toEqual({ items: [], previews: [], total: 0 })
})
})

describe('demoRecording', () => {
it('gives the card the Agent logo and the model, and readers the sentence', () => {
expect(demoRecording(demo())).toEqual({
Expand Down
Loading