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
3 changes: 3 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,9 @@ Rules for every texture:
| Skill | Name, then owner, then the curator note or the curator count |
| Collection | Name, description, curator, then the curator note or the first Skills |
| Curator | Avatar, name, handle with counts, description, then the first Skills |
| Demo | The demo's picture under a band: the lockup, then "What /{name} made" and the recording line. Its own frame, below |

**Demo card.** A demo page shares what the Agent made, so its card is a picture first, and `OgLayout` does not frame it. `OgImage/Demo.takumi.vue` puts the film's poster frame, or the page's first screen, in a window that fills the card below a band. The band has the lockup on the left. On the right, it has "What /{name} made" and the recording line. It has no Braille names field: the picture is the content. Rose inside the picture is depicted content, so the lockup's dot stays the card's one rose element.

### Motifs

Expand Down
83 changes: 83 additions & 0 deletions app/components/OgImage/Demo.takumi.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<script setup lang="ts">
import { ogCount, ogText } from '../../utils/og-props'
import { OG_INK, OG_MUTED, OG_RULE, OG_SURFACE } from '../../utils/og-style'

// Declared as the wire types the OG image URL can deliver, not as the types the
// template wants. A size arrives as a number or a string.
const props = defineProps<{
name?: string | number | null
recording?: string | number | null
image?: string | number | null
imageWidth?: string | number | null
imageHeight?: string | number | null
}>()

// A demo card is a window onto what the Agent made, under a band that names the
// demo and how it was recorded. The picture fills the window's width from its
// top edge, so a page shows its header and a film its frame, and the card's
// bottom edge cuts the rest.
const WIDTH = 1200
const GUTTER = 56
const BAND = 104
const WINDOW_WIDTH = WIDTH - GUTTER * 2
// Room for the title right of the lockup, and the average advance of one
// character, in em, of a title that is mostly a Skill name.
const TITLE_ROOM = 880
const TITLE_EM = 0.5

const safeTitle = computed(() => `What /${ogText(props.name)} made`)
// A Skill name runs to 64 characters, so a long one shrinks to keep one line.
const titleSize = computed(() => Math.max(20, Math.min(30, Math.floor(TITLE_ROOM / (safeTitle.value.length * TITLE_EM)))))
const safeRecording = computed(() => ogText(props.recording))
const safeImage = computed(() => ogText(props.image))
const scaledHeight = computed(() => {
const naturalWidth = ogCount(props.imageWidth)
const naturalHeight = ogCount(props.imageHeight)
return naturalWidth && naturalHeight ? Math.round(naturalHeight * WINDOW_WIDTH / naturalWidth) : 0
})
</script>

<template>
<div
data-theme="dark"
class="flex flex-col w-full h-full relative overflow-hidden font-sans"
:style="{ background: 'oklch(0.14 0.008 60)', color: OG_INK }"
>
<div
class="flex items-center justify-between shrink-0"
:style="{ height: `${BAND}px`, paddingLeft: `${GUTTER}px`, paddingRight: `${GUTTER}px` }"
>
<OgBrand :size="30" />
<div class="flex flex-col items-end" :style="{ gap: '6px' }">
<span :style="{ fontSize: `${titleSize}px`, fontWeight: 600, letterSpacing: '-0.03em', lineHeight: 1.1 }">
{{ safeTitle }}
</span>
<span v-if="safeRecording" class="font-mono" :style="{ fontSize: '18px', color: OG_MUTED }">
{{ safeRecording }}
</span>
</div>
</div>
<div
class="flex overflow-hidden"
:style="{
marginLeft: `${GUTTER}px`,
width: `${WINDOW_WIDTH}px`,
flexGrow: 1,
background: OG_SURFACE,
border: `1px solid ${OG_RULE}`,
borderBottom: 'none',
borderTopLeftRadius: '14px',
borderTopRightRadius: '14px',
}"
>
<img
v-if="safeImage && scaledHeight"
:src="safeImage"
alt=""
:width="WINDOW_WIDTH"
:height="scaledHeight"
:style="{ width: `${WINDOW_WIDTH}px`, height: `${scaledHeight}px`, flexShrink: 0 }"
>
</div>
</div>
</template>
32 changes: 30 additions & 2 deletions app/utils/home-demos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export interface HomeDemoItem {
model: string
/** `YYYY-MM-DD`. */
recordedAt: string
shots: { src: string, width: number, height: number, alt: string, viewport: 'desktop' | 'mobile' }[]
shots: HomeDemoShot[]
/** A Skill whose output is a film. The poster is a JPG frame; `src` is a muted H.264 MP4. */
/** The sandboxed output page of a page demo; null for a film. */
liveUrl: string | null
Expand All @@ -37,7 +37,15 @@ export interface HomeDemoVideo {
durationSeconds: number
}

export type HomeDemoShot = HomeDemoItem['shots'][number]
export interface HomeDemoShot {
src: string
width: number
height: number
alt: string
viewport: 'desktop' | 'mobile'
/** The first screen, when the shot runs longer than one. */
poster: { src: string, width: number, height: number } | null
}

/** Fewer than this reads as a broken feature, as with trending. */
export const HOME_DEMOS_MIN = 3
Expand Down Expand Up @@ -104,6 +112,26 @@ export function demoScreens(shot: HomeDemoShot): number {
return Math.max(1, Math.round(shot.height / screenHeight))
}

export interface DemoPicture {
src: string
width: number
height: number
}

/**
* The picture a demo page's social card shows: a film's poster frame, or the
* first screen of the desktop page. A long page never serves whole, since a
* 6000px shot would shrink to a strip. Undefined when the demo has neither;
* the page keeps its text card then.
*/
export function demoSocialPicture(demo: HomeDemoItem): DemoPicture | undefined {
if (demo.video)
return { src: demo.video.poster, width: demo.video.width, height: demo.video.height }
const shot = demo.shots.find(shot => shot.viewport === 'desktop')
const screen = shot?.poster ?? (shot && demoScreens(shot) === 1 ? shot : undefined)
return screen && { src: screen.src, width: screen.width, height: screen.height }
}

/** The Skill as `SkillCard` reads it, so a demo names its author the way every Skill embed does. */
export function demoCardSkill(demo: HomeDemoItem): SkillCardSkill {
return {
Expand Down
23 changes: 19 additions & 4 deletions layers/marketing/app/pages/skills/demos/[...demo].vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup lang="ts">
import type { HomeDemoItem } from '~~/app/utils/home-demos'
import { setResponseHeaders } from 'h3'
import { demoKey } from '~~/app/utils/home-demos'
import { demoKey, demoRecording, demoSocialPicture } from '~~/app/utils/home-demos'
import { resolveAuthorName } from '~~/app/utils/skill-byline'
import { demoNoun, groupDemos } from '#shared/demo-groups'
import { demoPagePath, DEMOS_PATH, MIN_INDEXABLE_DEMOS, parseDemoRoute } from '#shared/demo-pages'
Expand Down Expand Up @@ -107,9 +107,24 @@ useHead({
link: [{ rel: 'canonical', href: () => `https://skilld.dev${canonicalPath.value}` }],
})

defineOgImage('Page.takumi', pageDemo.value
? { title: `What /${pageDemo.value.name} made`, description: clip(pageDemo.value.prompt, 120) }
: { title: 'See what skills make', description: 'Recorded runs of agent skills: the prompt, and what the Agent made.' }, { alt: pageDemo.value ? `What /${pageDemo.value.name} made, on skilld` : 'Skill demos on skilld' })
// A shared demo page shows what the Agent made: the film's poster frame or the
// page's first screen. The board, and a demo without such a picture, keep the
// text card.
const socialPicture = pageDemo.value ? demoSocialPicture(pageDemo.value) : undefined
if (pageDemo.value && socialPicture) {
defineOgImage('Demo.takumi', {
name: pageDemo.value.name,
recording: demoRecording(pageDemo.value).sentence,
image: socialPicture.src,
imageWidth: socialPicture.width,
imageHeight: socialPicture.height,
}, { alt: `What /${pageDemo.value.name} made, on skilld` })
}
else {
defineOgImage('Page.takumi', pageDemo.value
? { title: `What /${pageDemo.value.name} made`, description: clip(pageDemo.value.prompt, 120) }
: { title: 'See what skills make', description: 'Recorded runs of agent skills: the prompt, and what the Agent made.' }, { alt: pageDemo.value ? `What /${pageDemo.value.name} made, on skilld` : 'Skill demos on skilld' })
}
</script>

<template>
Expand Down
36 changes: 32 additions & 4 deletions 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, formatDemoDuration } from '../../app/utils/home-demos'
import { demoPhoneRatio, demoRecording, demoSocialPicture, formatDemoDuration } from '../../app/utils/home-demos'

function demo(overrides: Partial<HomeDemoItem> = {}): HomeDemoItem {
return {
Expand All @@ -10,13 +10,14 @@ function demo(overrides: Partial<HomeDemoItem> = {}): HomeDemoItem {
skillPath: '/gh/emilkowalski/skills/emil-design-eng',
authorName: 'Emil Kowalski',
sourceUrl: 'https://github.com/emilkowalski/skills/blob/main/skills/emil-design-eng/SKILL.md',
makes: 'ui-component',
prompt: 'Build toast.html.',
agent: 'Claude Code',
model: 'claude-opus-5-5',
recordedAt: '2026-10-06',
shots: [
{ src: '/d.jpg', width: 1440, height: 900, alt: 'Desktop', viewport: 'desktop' },
{ src: '/m.jpg', width: 390, height: 844, alt: 'Phone', viewport: 'mobile' },
{ 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 },
],
...overrides,
}
Expand Down Expand Up @@ -50,11 +51,38 @@ describe('demoPhoneRatio', () => {
})

it('leaves a long phone page to the take frame', () => {
const long = demo({ shots: [demo().shots[0]!, { src: '/m.jpg', width: 390, height: 5091, alt: 'Phone', viewport: 'mobile' }] })
const long = demo({ shots: [demo().shots[0]!, { src: '/m.jpg', width: 390, height: 5091, alt: 'Phone', viewport: 'mobile', poster: null }] })
expect(demoPhoneRatio(long)).toBeUndefined()
})

it('never applies to a film', () => {
expect(demoPhoneRatio(demo({ video: { src: '/f.mp4', poster: '/p.jpg', width: 1280, height: 720, durationSeconds: 6 } }))).toBeUndefined()
})
})

describe('demoSocialPicture', () => {
const film = { src: 'https://media.example/f.mp4', poster: 'https://media.example/p.jpg', width: 1280, height: 720, durationSeconds: 6 }

it('shows a one-screen page as its desktop shot', () => {
expect(demoSocialPicture(demo())).toEqual({ src: '/d.jpg', width: 1440, height: 900 })
})

it('shows a long page as its first screen, never the whole page', () => {
const long = demo({
shots: [{ src: '/d.jpg', width: 1440, height: 6000, alt: 'Desktop', viewport: 'desktop', poster: { src: '/d-poster.jpg', width: 1440, height: 900 } }],
})
expect(demoSocialPicture(long)).toEqual({ src: '/d-poster.jpg', width: 1440, height: 900 })
})

it('shows a film as its poster frame', () => {
expect(demoSocialPicture(demo({ video: film }))).toEqual({ src: 'https://media.example/p.jpg', width: 1280, height: 720 })
})

it('gives no picture for a long page that has no first screen', () => {
expect(demoSocialPicture(demo({ shots: [{ src: '/d.jpg', width: 1440, height: 6000, alt: 'Desktop', viewport: 'desktop', poster: null }] }))).toBeUndefined()
})

it('gives no picture for a demo recorded only on a phone', () => {
expect(demoSocialPicture(demo({ shots: [{ src: '/m.jpg', width: 390, height: 844, alt: 'Phone', viewport: 'mobile', poster: null }] }))).toBeUndefined()
})
})
Loading