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: 2 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"media:prepare-showcase": "node scripts/prepare-showcase-retail.mjs",
"media:capture-showcase": "node scripts/capture-showcase.mjs",
"media:showcase": "tsx scripts/generate-showcase.ts",
"media:verify-showcase": "node scripts/verify-showcase.mjs",
"media:verify-showcase": "pnpm media:verify-home",
"media:verify-home": "node scripts/verify-home.mjs",
"metrics:update-fallback": "tsx scripts/generate-metrics.ts --update-fallback",
"og:generate": "tsx scripts/generate-og.ts",
"preview": "astro preview",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { fileURLToPath } from 'node:url'
import { chromium } from '@playwright/test'
import sharp from 'sharp'

const output = new URL('../.cache/showcase-qa/', import.meta.url)
const before = new URL('../.cache/visual-qa/', import.meta.url)
const output = new URL('../.cache/demo-qa/', import.meta.url)
const before = new URL('../.cache/showcase-qa/', import.meta.url)
const baseURL = process.env.SHOWCASE_BASE_URL ?? 'http://127.0.0.1:4321'
const routes = ['/', '/en/', '/projects/weapp-tailwindcss/', '/projects/weapp-vite/', '/projects/varo/', '/pricing/', '/en/pricing/']
const records = []
Expand Down Expand Up @@ -52,6 +52,25 @@ try {
{ input: fileURLToPath(new URL(`${name}.png`, output)), left: viewport.width, top: 0 },
]).png().toFile(fileURLToPath(new URL(`${name}-comparison.png`, output)))
}
const lab = page.locator('hero-demos')
for (const [index, kind] of ['style', 'build', 'registry'].entries()) {
await lab.getByRole('tab').nth(index).click()
if (kind === 'style') {
await lab.locator('style-demo input[value="1"]').check()
await lab.locator('style-demo select').selectOption('2')
await lab.locator('[data-compact]').check()
}
if (kind === 'build') {
await lab.locator('build-demo input[value="2"]').check()
}
if (kind === 'registry') {
await lab.locator('registry-demo input[value="card"]').uncheck()
await lab.locator('[data-registry-button]').click()
}
await page.screenshot({ path: fileURLToPath(new URL(`${name}-${kind}.png`, output)) })
await lab.screenshot({ path: fileURLToPath(new URL(`${name}-${kind}-detail.png`, output)) })
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > document.documentElement.clientWidth + 1), false)
}
}
records.push({ route, viewport, theme, name, ...checks, errors })
page.off('pageerror', onError)
Expand Down
19 changes: 0 additions & 19 deletions apps/web/src/components/ShowcasePicture.astro

This file was deleted.

21 changes: 7 additions & 14 deletions apps/web/src/components/home/HomeHero.astro
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { Locale } from '../../types/project'
import type { HomeProject } from '../../lib/home-projects'
import { localizePath, siteCopy } from '../../i18n/ui'
import { buttonPrimary, buttonSecondary, shell } from '../../styles/classes'
import ShowcasePicture from '../ShowcasePicture.astro'
import HeroDemos from './demos/HeroDemos.astro'

interface Props {
locale: Locale
Expand All @@ -13,17 +13,15 @@ interface Props {

const { locale, projects } = Astro.props
const copy = siteCopy[locale]
const heroRatios = projects.map(project => project.showcase[0].width / project.showcase[0].height)
const heroRatioTotal = heroRatios.reduce((sum, ratio) => sum + ratio, 0)
---

<section class="home-hero">
<div class:list={[shell, 'home-hero-grid']}>
<div class="home-hero-copy">
<p data-hero-enter style="--enter-delay: 0ms" class="home-kicker">{copy.hero.eyebrow}</p>
<h1 data-hero-enter style="--enter-delay: 45ms" class="home-hero-title">{copy.hero.title}<span class="home-hero-dot" aria-hidden="true">.</span></h1>
<p data-hero-enter style="--enter-delay: 90ms" class="home-hero-description">{copy.hero.description}</p>
<div data-hero-enter style="--enter-delay: 135ms" class="flex flex-wrap items-center gap-3">
<p class="home-kicker">{copy.hero.eyebrow}</p>
<h1 class="home-hero-title">{copy.hero.title}<span class="home-hero-dot" aria-hidden="true">.</span></h1>
<p class="home-hero-description">{copy.hero.description}</p>
<div class="flex flex-wrap items-center gap-3">
<a class={buttonPrimary} href={`${localizePath(locale)}#projects`} data-analytics-event="navigate_section" data-analytics-section="projects">
{copy.hero.primary}
<ArrowRight size={18} stroke-width={1.9} aria-hidden="true" />
Expand All @@ -36,20 +34,15 @@ const heroRatioTotal = heroRatios.reduce((sum, ratio) => sum + ratio, 0)
</div>

<div class="home-hero-stage" aria-label={copy.hero.visualLabel}>
{projects.map((project, index) => (
<figure class="home-hero-example" data-hero-example={project.id} style={`--showcase-ratio: ${project.showcase[0].width / project.showcase[0].height}`}>
<ShowcasePicture visual={project.showcase[0]} locale={locale} sizes={`(max-width: 720px) ${330 * heroRatios[index]}px, min(${390 * heroRatios[index]}px, calc((100vw - 112px) * ${heroRatios[index] / heroRatioTotal}))`} priority={index === 0} />
<figcaption><span>{project.data.locales[locale].name}</span><span>{project.stage[locale]}</span></figcaption>
</figure>
))}
<HeroDemos locale={locale} projects={projects} />
</div>
</div>

<div class:list={[shell, 'home-project-rail']} aria-label={copy.nav.projects}>
{projects.map((project, index) => {
const content = project.data.locales[locale]
return (
<a data-hero-enter style={`--enter-delay: ${220 + index * 45}ms`} class="home-project-link" href={project.data.docsUrl} target="_blank" rel="noreferrer" data-analytics-event="click_outbound" data-analytics-target="project_home" data-analytics-project={project.id}>
<a class="home-project-link" href={project.data.docsUrl} target="_blank" rel="noreferrer" data-analytics-event="click_outbound" data-analytics-target="project_home" data-analytics-project={project.id}>
<span class="home-project-index">{String(index + 1).padStart(2, '0')}</span>
<img src={project.data.logo} width="34" height="34" alt="" />
<span class="min-w-0"><strong>{content.name}</strong><small>{project.stage[locale]} / {content.tagline}</small></span>
Expand Down
23 changes: 11 additions & 12 deletions apps/web/src/components/home/HomeProjectRow.astro
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
---
import { ArrowRight, BadgeCheck, BookOpen } from '@lucide/astro'
import { ArrowRight, ArrowUpRight, BadgeCheck, BookOpen } from '@lucide/astro'
import type { Locale, ProjectMetrics } from '../../types/project'
import type { HomeProject } from '../../lib/home-projects'
import { formatCompactNumber } from '../../lib/metrics'
import { projectPath, siteCopy } from '../../i18n/ui'
import { buttonPrimary, buttonSecondary, projectTone } from '../../styles/classes'
import ShowcasePicture from '../ShowcasePicture.astro'
import { showcaseSizes } from './media-sizes'
import HomeDemo from './demos/HomeDemo.astro'
import { demoCopy } from './demos/copy'

interface Props {
locale: Locale
Expand All @@ -22,16 +22,15 @@ const numberLocale = locale === 'zh-CN' ? 'zh-CN' : 'en-US'
---

<article data-project-row class:list={['home-project-row', { 'home-project-row-reversed': project.reversed }]} style={`--project-accent: ${project.data.accent}`}>
<a class="home-project-visual-link" href={project.data.docsUrl} target="_blank" rel="noreferrer" data-analytics-event="click_outbound" data-analytics-target="project_home_visual" data-analytics-project={project.id} aria-label={locale === 'zh-CN' ? `打开 ${content.name} 官方站点` : `Open ${content.name} official site`}>
<div class="home-showcase-gallery" data-showcase-count={project.showcase.length}>
{project.showcase.map(visual => (
<figure data-reveal data-project-visual class:list={['home-showcase-figure', { 'home-showcase-phone': project.layout !== 'component' }]}>
<ShowcasePicture visual={visual} locale={locale} sizes={showcaseSizes[project.layout]} />
<figcaption>{visual.locales[locale].caption}</figcaption>
</figure>
))}
<div class="home-project-demo">
<div class="home-demo-heading">
<span>{content.name}</span>
<a class="home-project-demo-link" href={project.data.docsUrl} target="_blank" rel="noreferrer" data-analytics-event="click_outbound" data-analytics-target="project_home_visual" data-analytics-project={project.id} aria-label={locale === 'zh-CN' ? `打开 ${content.name} 官方站点` : `Open ${content.name} official site`}>
{demoCopy[locale].official}<ArrowUpRight size={16} aria-hidden="true" />
</a>
</div>
</a>
<div class="home-lab"><HomeDemo kind={project.demo} locale={locale} instance={`project-${project.id}`} /></div>
</div>

<div data-reveal data-project-copy style={`--reveal-delay: ${80 + index * 60}ms`}>
<p class="mb-4 font-mono text-[10px] font-bold text-brand">{String(index + 1).padStart(2, '0')} / {project.stage[locale]}</p>
Expand Down
54 changes: 54 additions & 0 deletions apps/web/src/components/home/demos/BuildDemo.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
import { Folder, FileCode } from '@lucide/astro'
import type { Locale } from '../../../types/project'
import CodePanel from './CodePanel.astro'
import { demoCopy } from './copy'
import { buildExample, buildTargets } from './build'

interface Props { locale: Locale, instance: string }
const { locale, instance } = Astro.props
const copy = demoCopy[locale]
const example = buildExample(0)
---
<build-demo class="home-demo" data-demo="build">
<div class="demo-controls-space">
<fieldset class="demo-controls" disabled hidden>
<legend class="sr-only">{copy.target}</legend>
<div class="demo-segments">{buildTargets.map((target, index) => <label data-target={target.id}><input type="radio" name={`${instance}-target`} value={index} checked={index === 0} /><span>{copy.targets[index]}</span></label>)}</div>
</fieldset>
</div>
<div class="demo-body">
<CodePanel parts={example.parts} locale={locale} filename="vite.config.ts" />
<div class="demo-preview demo-build-output">
<div class="demo-pane-heading">{copy.output}</div>
<div class="demo-file-tree">
<div class="demo-directory"><Folder size={20} aria-hidden="true" /><code data-directory>{example.directory}/</code></div>
<div class="demo-tree-branch"><span>pages/index/</span><ul>{example.files.map((file, index) => <li><FileCode size={16} aria-hidden="true" /><code data-output-file={index}>{file}</code></li>)}</ul></div>
</div>
<code class="demo-build-command" data-build-command>{example.command}</code>
<div class="demo-note">{copy.buildNote}</div>
</div>
</div>
</build-demo>
<script>
import { updateCode } from './code'
import { buildExample } from './build'

class BuildDemo extends HTMLElement {
connectedCallback() {
if (this.dataset.ready) return
const controls = this.querySelector('fieldset')!
controls.addEventListener('change', () => {
const example = buildExample(Number(this.querySelector<HTMLInputElement>('input:checked')!.value))
updateCode(this, example.parts)
this.querySelector('[data-directory]')!.textContent = `${example.directory}/`
this.querySelector('[data-build-command]')!.textContent = example.command
this.querySelectorAll('[data-output-file]').forEach((file, index) => { file.textContent = example.files[index] })
})
controls.disabled = false
controls.hidden = false
this.dataset.ready = 'true'
}
}
customElements.define('build-demo', BuildDemo)
</script>
40 changes: 40 additions & 0 deletions apps/web/src/components/home/demos/CodePanel.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
import { Copy } from '@lucide/astro'
import type { Locale } from '../../../types/project'
import type { CodePart } from './code'
import { demoCopy } from './copy'

interface Props { parts: CodePart[], locale: Locale, filename: string }
const { parts, locale, filename } = Astro.props
const copy = demoCopy[locale]
---
<demo-code class="demo-code" data-locale={locale}>
<div class="demo-pane-heading"><span>{filename}</span><button data-copy type="button" class="demo-icon-button" aria-label={copy.copy} title={copy.copy} hidden><Copy size={16} aria-hidden="true" /></button></div>
<pre tabindex="0" aria-label={copy.source}><code>{parts.map(part => <span class={part.tone ? `demo-syntax-${part.tone}` : undefined}>{part.text}</span>)}</code></pre>
<span class="demo-copy-status" role="status"></span>
</demo-code>
<script>
import { demoCopy } from './copy'
import type { Locale } from '../../../types/project'

class DemoCode extends HTMLElement {
connectedCallback() {
if (this.dataset.ready) return
const button = this.querySelector<HTMLButtonElement>('[data-copy]')!
const status = this.querySelector<HTMLElement>('[role="status"]')!
const copy = demoCopy[this.dataset.locale as Locale]
button.addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(this.querySelector('code')!.textContent ?? '')
status.textContent = copy.copied
}
catch {
status.textContent = copy.copyFailed
}
})
button.hidden = false
this.dataset.ready = 'true'
}
}
customElements.define('demo-code', DemoCode)
</script>
49 changes: 49 additions & 0 deletions apps/web/src/components/home/demos/HeroDemos.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
import type { Locale } from '../../../types/project'
import type { HomeProject } from '../../../lib/home-projects'
import HomeDemo from './HomeDemo.astro'
import { demoCopy } from './copy'

interface Props { locale: Locale, projects: HomeProject[] }
const { locale, projects } = Astro.props
const copy = demoCopy[locale]
---
<hero-demos class="home-lab" aria-label={copy.playground}>
<div class="home-lab-heading">
<div class="home-lab-tabs" role="tablist" aria-label={copy.playground} hidden>
{projects.map((project, index) => <button type="button" role="tab" id={`hero-tab-${project.demo}`} aria-controls={`hero-panel-${project.demo}`} aria-selected={index === 0 ? 'true' : 'false'} tabindex={index === 0 ? 0 : -1}>{copy.tabs[index]}</button>)}
</div>
<span class="home-lab-project" data-active-project>{projects[0].data.locales[locale].name}</span>
</div>
{projects.map((project, index) => <div role="tabpanel" id={`hero-panel-${project.demo}`} aria-labelledby={`hero-tab-${project.demo}`} hidden={index !== 0} data-project-name={project.data.locales[locale].name} tabindex="0"><HomeDemo kind={project.demo} locale={locale} instance={`hero-${project.demo}`} /></div>)}
</hero-demos>
<script>
class HeroDemos extends HTMLElement {
connectedCallback() {
if (this.dataset.ready) return
const tabs = [...this.querySelectorAll<HTMLButtonElement>('[role="tab"]')]
const panels = [...this.querySelectorAll<HTMLElement>('[role="tabpanel"]')]
const activate = (index: number) => {
tabs.forEach((tab, tabIndex) => {
tab.setAttribute('aria-selected', String(tabIndex === index))
tab.tabIndex = tabIndex === index ? 0 : -1
panels[tabIndex].hidden = tabIndex !== index
})
this.querySelector('[data-active-project]')!.textContent = panels[index].dataset.projectName!
}
tabs.forEach((tab, index) => {
tab.addEventListener('click', () => activate(index))
tab.addEventListener('keydown', (event) => {
const next = { ArrowRight: (index + 1) % tabs.length, ArrowLeft: (index + tabs.length - 1) % tabs.length, Home: 0, End: tabs.length - 1 }[event.key]
if (next === undefined) return
event.preventDefault()
activate(next)
tabs[next].focus()
})
})
this.querySelector<HTMLElement>('[role="tablist"]')!.hidden = false
this.dataset.ready = 'true'
}
}
customElements.define('hero-demos', HeroDemos)
</script>
14 changes: 14 additions & 0 deletions apps/web/src/components/home/demos/HomeDemo.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
import type { Locale } from '../../../types/project'
import type { HomeDemoKind } from '../../../lib/home-projects'
import StyleDemo from './StyleDemo.astro'
import BuildDemo from './BuildDemo.astro'
import RegistryDemo from './RegistryDemo.astro'
import './demo.css'

interface Props { kind: HomeDemoKind, locale: Locale, instance: string }
const { kind, locale, instance } = Astro.props
const components = { style: StyleDemo, build: BuildDemo, registry: RegistryDemo }
const Demo = components[kind]
---
<Demo locale={locale} instance={instance} />
Loading
Loading