Compare commits

...

11 Commits

Author SHA1 Message Date
DrJKL
ffe1b4ce9d Load pinned sections on load to prevent pop-in scrolling. 2026-04-15 13:34:39 -07:00
DrJKL
0fa90b0177 fix: prevent UseCaseSection bubble clipping during pin+scrub
- Remove paddingBottom from vpH in usePinScrub so content text and CTA
  button are not clipped at the bottom of the pinned viewport
- Increase section height to calc(100vh+60px) to contain left bubble at
  rest (top-80 + h-160 = 960px exceeds 100vh on short viewports)
- Split parallax: right bubble keeps y:200 (down), left bubble uses
  y:-60 (up) so neither overflows the section with overflow-hidden

Amp-Thread-ID: https://ampcode.com/threads/T-019d926d-2a12-726c-befa-fa62ffd1d112
Co-authored-by: Amp <amp@ampcode.com>
2026-04-15 12:00:58 -07:00
DrJKL
2b4afd132f fix: account for section padding in UseCaseSection pin scrub
vpH used window.innerHeight but the section has vertical padding,
causing interpolateY to underestimate the scroll needed to reveal
the bottom content (body text and CTA button).

Amp-Thread-ID: https://ampcode.com/threads/T-019d9234-bf54-705c-80ee-e70e179d3e90
Co-authored-by: Amp <amp@ampcode.com>
2026-04-15 10:39:50 -07:00
DrJKL
0b35b7acfc empty 2026-04-15 10:31:22 -07:00
Alexander Brown
42cc892869 Merge branch 'main' into feat/website-api-enterprise 2026-04-15 10:26:13 -07:00
DrJKL
b25d8c23c8 fix: scrub hero logo from page top to canvas bottom
Amp-Thread-ID: https://ampcode.com/threads/T-019d9220-9fe3-7190-b7ff-df138f73be60
Co-authored-by: Amp <amp@ampcode.com>
2026-04-15 10:23:09 -07:00
Yourz
e10dfb98eb feat: website api and enterprise page 2026-04-16 00:34:55 +08:00
Yourz
97c50a30a7 fix: update for coderabbitai 2026-04-15 22:57:39 +08:00
Yourz
10f0602b20 feat: extract productCardsSection: 2026-04-15 22:57:39 +08:00
Yourz
c7833ca5f1 fix: update for coderabbitai, and make FAQ component props as translate key 2026-04-15 22:57:38 +08:00
Yourz
47293e1203 feat(website): implement download/local page
- Add HeroSection, CloudBannerSection, ReasonSection, EcoSystemSection,
  ProductCardsSection, FAQSection components
- Add ProductHeroBadge with two-size node badge variant
- Extend NodeBadge with per-segment class and configurable union icon
- Add useDownloadUrl composable for OS-aware download links
- Add all en/zh-CN translations for the download page
- Replace zh-CN hardcoded download page with shared components
- Add icon-mask utility for CSS mask-based icons
- Add Playwright e2e tests (smoke, interaction, mobile)
- FAQ uses independent expand/collapse (all open by default)
- Sticky headings in ReasonSection and FAQSection on all viewports
2026-04-15 22:57:38 +08:00
44 changed files with 2259 additions and 147 deletions

View File

@@ -0,0 +1,167 @@
import { expect, test } from '@playwright/test'
test.describe('Download page @smoke', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/download')
})
test('has correct title', async ({ page }) => {
await expect(page).toHaveTitle('Download Comfy — Run AI Locally')
})
test('CloudBannerSection is visible with cloud link', async ({ page }) => {
const link = page.getByRole('link', { name: /TRY COMFY CLOUD/i })
await expect(link).toBeVisible()
await expect(link).toHaveAttribute('href', 'https://app.comfy.org')
})
test('HeroSection heading and subtitle are visible', async ({ page }) => {
await expect(
page.getByRole('heading', { name: /Run on your hardware/i, level: 1 })
).toBeVisible()
await expect(page.getByText(/The full ComfyUI engine/)).toBeVisible()
})
test('HeroSection has download and GitHub buttons', async ({ page }) => {
const hero = page.locator('section', {
has: page.getByRole('heading', {
name: /Run on your hardware/i,
level: 1
})
})
const downloadBtn = hero.getByRole('link', { name: /DOWNLOAD LOCAL/i })
await expect(downloadBtn).toBeVisible()
await expect(downloadBtn).toHaveAttribute('target', '_blank')
const githubBtn = hero.getByRole('link', { name: /INSTALL FROM GITHUB/i })
await expect(githubBtn).toBeVisible()
await expect(githubBtn).toHaveAttribute(
'href',
'https://github.com/Comfy-Org/ComfyUI'
)
})
test('ReasonSection heading and reasons are visible', async ({ page }) => {
await expect(
page.getByRole('heading', { name: /Why.*professionals.*choose/i })
).toBeVisible()
for (const title of [
'Unlimited',
'Any model',
'Your machine',
'Free. Open Source'
]) {
await expect(page.getByText(title).first()).toBeVisible()
}
})
test('EcoSystemSection heading is visible', async ({ page }) => {
await expect(page.getByText(/An ecosystem that moves faster/)).toBeVisible()
})
test('ProductCardsSection has 3 product cards', async ({ page }) => {
const section = page.locator('section', {
has: page.getByRole('heading', { name: /The AI creation/ })
})
const cards = section.locator('a[href]')
await expect(cards).toHaveCount(3)
})
test('ProductCardsSection links to cloud, api, enterprise', async ({
page
}) => {
const section = page.locator('section', {
has: page.getByRole('heading', { name: /The AI creation/ })
})
for (const href of ['/cloud', '/api', '/cloud/enterprise']) {
await expect(section.locator(`a[href="${href}"]`)).toBeVisible()
}
})
test('FAQSection heading is visible with 8 items', async ({ page }) => {
await expect(page.getByRole('heading', { name: /FAQ/i })).toBeVisible()
const faqButtons = page.locator('button[aria-controls^="faq-panel-"]')
await expect(faqButtons).toHaveCount(8)
})
})
test.describe('FAQ accordion @interaction', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/download')
})
test('all FAQs are expanded by default', async ({ page }) => {
await expect(
page.getByText(/A dedicated GPU is strongly recommended/i)
).toBeVisible()
await expect(page.getByText(/ComfyUI is lightweight/i)).toBeVisible()
})
test('clicking an expanded FAQ collapses it', async ({ page }) => {
const firstQuestion = page.getByRole('button', {
name: /Do I need a GPU/i
})
await firstQuestion.scrollIntoViewIfNeeded()
await firstQuestion.click()
await expect(
page.getByText(/A dedicated GPU is strongly recommended/i)
).toBeHidden()
})
test('clicking a collapsed FAQ expands it again', async ({ page }) => {
const firstQuestion = page.getByRole('button', {
name: /Do I need a GPU/i
})
await firstQuestion.scrollIntoViewIfNeeded()
await firstQuestion.click()
await expect(
page.getByText(/A dedicated GPU is strongly recommended/i)
).toBeHidden()
await firstQuestion.click()
await expect(
page.getByText(/A dedicated GPU is strongly recommended/i)
).toBeVisible()
})
})
test.describe('Download page mobile @mobile', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/download')
})
test('CloudBannerSection is visible', async ({ page }) => {
await expect(page.getByText(/Need more power/)).toBeVisible()
})
test('HeroSection heading is visible', async ({ page }) => {
await expect(
page.getByRole('heading', { name: /Run on your hardware/i, level: 1 })
).toBeVisible()
})
test('download buttons are stacked vertically', async ({ page }) => {
const hero = page.locator('section', {
has: page.getByRole('heading', {
name: /Run on your hardware/i,
level: 1
})
})
const downloadBtn = hero.getByRole('link', { name: /DOWNLOAD LOCAL/i })
const githubBtn = hero.getByRole('link', { name: /INSTALL FROM GITHUB/i })
await downloadBtn.scrollIntoViewIfNeeded()
const downloadBox = await downloadBtn.boundingBox()
const githubBox = await githubBtn.boundingBox()
expect(downloadBox, 'download button bounding box').not.toBeNull()
expect(githubBox, 'github button bounding box').not.toBeNull()
expect(githubBox!.y).toBeGreaterThan(downloadBox!.y)
})
})

View File

@@ -0,0 +1,3 @@
<svg width="62" height="94.14" viewBox="0 0 62 80" preserveAspectRatio="none" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22.9346 0C33.456 0.000149153 39.6242 8.20368 36.7305 18.3115L33.9385 28.0635C32.7454 32.2159 35.8674 36.3555 40.1826 36.3555C42.9814 36.3555 45.4493 34.5653 46.3311 31.9268L47.7129 27.002C49.4225 20.9287 55.812 16 62 16V64H48.5342C42.3461 64 38.7182 59.0713 40.4199 52.998L40.8398 51.5L40.8301 51.4922C42.0104 47.3146 38.8756 43.1751 34.5352 43.1748C31.6287 43.1748 29.0515 45.1048 28.252 47.9111L24.3047 61.6885H24.2793C21.3855 71.7964 10.5089 80 0 80V0H22.9346Z" fill="#F2FF59"/>
</svg>

After

Width:  |  Height:  |  Size: 625 B

View File

@@ -0,0 +1,32 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import FAQSection from './FAQSection.vue'
const meta: Meta<typeof FAQSection> = {
title: 'Website/Common/FAQSection',
component: FAQSection,
tags: ['autodocs'],
decorators: [
() => ({
template: '<div class="bg-primary-comfy-ink p-8"><story /></div>'
})
],
args: {
headingKey: 'download.faq.heading',
faqPrefix: 'download.faq',
faqCount: 3
}
}
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {}
export const ManyItems: Story = {
args: {
headingKey: 'download.faq.heading',
faqPrefix: 'download.faq',
faqCount: 8
}
}

View File

@@ -0,0 +1,98 @@
<script setup lang="ts">
import { reactive } from 'vue'
import type { Locale, TranslationKey } from '../../i18n/translations'
import { t } from '../../i18n/translations'
const {
locale = 'en',
headingKey,
faqPrefix,
faqCount
} = defineProps<{
locale?: Locale
headingKey: TranslationKey
faqPrefix: string
faqCount: number
}>()
const faqKeys: Array<{ q: TranslationKey; a: TranslationKey }> = Array.from(
{ length: faqCount },
(_, i) => ({
q: `${faqPrefix}.${i + 1}.q` as TranslationKey,
a: `${faqPrefix}.${i + 1}.a` as TranslationKey
})
)
const faqs = faqKeys.map(({ q, a }) => ({
question: t(q, locale),
answer: t(a, locale)
}))
const expanded = reactive(faqs.map(() => true))
function toggle(index: number) {
expanded[index] = !expanded[index]
}
</script>
<template>
<section class="px-4 py-24 md:px-20 md:py-40">
<div class="flex flex-col gap-6 md:flex-row md:gap-16">
<!-- Left heading -->
<div
class="bg-primary-comfy-ink sticky top-20 z-10 w-full shrink-0 self-start py-4 md:top-28 md:w-80 md:py-0"
>
<h2 class="text-primary-comfy-canvas text-4xl font-light md:text-5xl">
{{ t(headingKey, locale) }}
</h2>
</div>
<!-- Right FAQ list -->
<div class="flex-1">
<div
v-for="(faq, index) in faqs"
:key="index"
class="border-primary-comfy-canvas/20 border-b"
>
<button
:id="`faq-trigger-${index}`"
:aria-expanded="expanded[index]"
:aria-controls="`faq-panel-${index}`"
class="flex w-full cursor-pointer items-center justify-between text-left"
:class="index === 0 ? 'pb-6' : 'py-6'"
@click="toggle(index)"
>
<span
class="text-lg font-light md:text-xl"
:class="
expanded[index]
? 'text-primary-comfy-yellow'
: 'text-primary-comfy-canvas'
"
>
{{ faq.question }}
</span>
<span
class="text-primary-comfy-yellow ml-4 shrink-0 text-2xl"
aria-hidden="true"
>
{{ expanded[index] ? '' : '+' }}
</span>
</button>
<section
v-if="expanded[index]"
:id="`faq-panel-${index}`"
:aria-labelledby="`faq-trigger-${index}`"
class="pb-6"
>
<p class="text-primary-comfy-canvas/70 text-sm whitespace-pre-line">
{{ faq.answer }}
</p>
</section>
</div>
</div>
</div>
</section>
</template>

View File

@@ -0,0 +1,74 @@
<script setup lang="ts">
import type { Locale, TranslationKey } from '../../i18n/translations'
import { getRoutes } from '../../config/routes'
import { t } from '../../i18n/translations'
import ProductCard from './ProductCard.vue'
type Product = 'local' | 'cloud' | 'api' | 'enterprise'
const {
locale = 'en',
excludeProduct,
labelKey = 'products.label'
} = defineProps<{
locale?: Locale
excludeProduct?: Product
labelKey?: TranslationKey
}>()
const routes = getRoutes(locale)
const allCards: (ReturnType<typeof cardDef> & { product: Product })[] = [
cardDef('local', routes.download, 'bg-primary-warm-gray'),
cardDef('cloud', routes.cloud, 'bg-secondary-mauve'),
cardDef('api', routes.api, 'bg-primary-comfy-plum'),
cardDef('enterprise', routes.cloudEnterprise, 'bg-illustration-forest')
]
function cardDef(product: Product, href: string, bg: string) {
return {
product,
title: t(`products.${product}.title`, locale),
description: t(`products.${product}.description`, locale),
cta: t(`products.${product}.cta`, locale),
href,
bg
}
}
const cards = excludeProduct
? allCards.filter((c) => c.product !== excludeProduct)
: allCards
</script>
<template>
<section class="bg-primary-comfy-ink px-4 py-20 lg:px-20 lg:py-24">
<!-- Header -->
<div class="flex flex-col items-center text-center">
<p
class="text-primary-comfy-yellow text-xs font-bold tracking-widest uppercase"
>
{{ t(labelKey, locale) }}
</p>
<h2
class="text-primary-comfy-canvas mt-4 text-4xl font-light whitespace-pre-line lg:text-5xl"
>
{{ t('products.heading', locale) }}
</h2>
<p class="text-primary-comfy-canvas/70 mt-4 text-sm">
{{ t('products.subheading', locale) }}
</p>
</div>
<!-- Cards -->
<div
:class="[
'mt-16 grid grid-cols-1 gap-4',
cards.length === 4 ? 'lg:grid-cols-4' : 'lg:grid-cols-3'
]"
>
<ProductCard v-for="card in cards" :key="card.product" v-bind="card" />
</div>
</section>
</template>

View File

@@ -0,0 +1,56 @@
<script setup lang="ts">
const {
logoSrc = '/icons/logo.svg',
logoAlt = 'Comfy',
text = 'LOCAL'
} = defineProps<{
logoSrc?: string
logoAlt?: string
text?: string
}>()
</script>
<template>
<div class="font-formula-condensed flex items-stretch font-semibold">
<img
src="/icons/node-left.svg"
alt=""
class="-mx-px my-auto h-12 self-center lg:my-0 lg:h-auto lg:self-stretch"
aria-hidden="true"
/>
<span
class="bg-primary-comfy-yellow text-primary-comfy-ink my-auto flex h-12 items-center justify-center lg:my-0 lg:h-auto lg:p-8"
>
<img
:src="logoSrc"
:alt="logoAlt"
class="inline-block h-6 brightness-0 lg:h-10"
/>
</span>
<img
src="/icons/node-union-2size.svg"
alt=""
class="-mx-px my-auto h-12 self-center lg:my-0 lg:h-auto lg:self-stretch"
aria-hidden="true"
/>
<span
class="bg-primary-comfy-yellow text-primary-comfy-ink my-auto flex h-7.25 items-center justify-center lg:h-15.5 lg:px-6"
>
<span
class="inline-block translate-y-0.5 text-2xl leading-none font-bold lg:text-3xl"
>
{{ text }}
</span>
</span>
<img
src="/icons/node-right.svg"
alt=""
class="-mx-px my-auto h-7.25 self-center lg:h-15.5"
aria-hidden="true"
/>
</div>
</template>

View File

@@ -14,9 +14,12 @@ useFrameScrub(canvasRef, {
frameSrc: (i) =>
`/videos/hero-logo-seq/Logo${String(i).padStart(2, '0')}.webp`,
scrollTrigger: (canvas) => ({
trigger: canvas,
start: 'top 80%',
end: 'bottom 20%',
trigger: document.documentElement,
start: 'top top',
end: () => {
const rect = canvas.getBoundingClientRect()
return `+=${rect.bottom + window.scrollY}`
},
scrub: 0.3
})
})

View File

@@ -1,67 +1,11 @@
<script setup lang="ts">
import type { Locale } from '../../i18n/translations'
import { getRoutes } from '../../config/routes'
import { t } from '../../i18n/translations'
import ProductCard from '../common/ProductCard.vue'
import ProductCardsSection from '../common/ProductCardsSection.vue'
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
const routes = getRoutes(locale)
const cards = [
{
title: t('products.local.title', locale),
description: t('products.local.description', locale),
cta: t('products.local.cta', locale),
href: routes.download,
bg: 'bg-primary-warm-gray'
},
{
title: t('products.cloud.title', locale),
description: t('products.cloud.description', locale),
cta: t('products.cloud.cta', locale),
href: routes.cloud,
bg: 'bg-secondary-mauve'
},
{
title: t('products.api.title', locale),
description: t('products.api.description', locale),
cta: t('products.api.cta', locale),
href: routes.api,
bg: 'bg-primary-comfy-plum'
},
{
title: t('products.enterprise.title', locale),
description: t('products.enterprise.description', locale),
cta: t('products.enterprise.cta', locale),
href: routes.cloudEnterprise,
bg: 'bg-illustration-forest'
}
]
</script>
<template>
<section class="bg-primary-comfy-ink px-4 py-20 lg:px-20 lg:py-24">
<!-- Header -->
<div class="flex flex-col items-center text-center">
<p
class="text-primary-comfy-yellow text-xs font-bold tracking-widest uppercase"
>
{{ t('products.label', locale) }}
</p>
<h2
class="text-primary-comfy-canvas mt-4 text-4xl font-light whitespace-pre-line lg:text-5xl"
>
{{ t('products.heading', locale) }}
</h2>
<p class="text-primary-warm-gray mt-4 text-sm">
{{ t('products.subheading', locale) }}
</p>
</div>
<!-- Cards -->
<div class="mt-16 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<ProductCard v-for="card in cards" :key="card.title" v-bind="card" />
</div>
</section>
<ProductCardsSection :locale="locale" />
</template>

View File

@@ -29,13 +29,14 @@ const { activeIndex: activeCategory } = usePinScrub(
{ itemCount: categories.length }
)
useParallax([leftImgRef, rightImgRef], { trigger: sectionRef })
useParallax([rightImgRef], { trigger: sectionRef })
useParallax([leftImgRef], { trigger: sectionRef, y: -60 })
</script>
<template>
<section
ref="sectionRef"
class="bg-primary-comfy-ink relative flex flex-col items-center overflow-hidden px-8 py-20 lg:h-screen lg:px-0 lg:py-24"
class="bg-primary-comfy-ink relative flex flex-col items-center overflow-hidden px-8 py-20 lg:h-[calc(100vh+60px)] lg:px-0 lg:py-24"
>
<!-- Left image -->
<div

View File

@@ -0,0 +1,33 @@
<script setup lang="ts">
import type { Locale } from '../../../i18n/translations'
import { t } from '../../../i18n/translations'
import FeatureShowcaseSection from '../shared/FeatureShowcaseSection.vue'
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
const features = [
{
title: t('api.automation.feature1.title', locale),
description: t('api.automation.feature1.description', locale),
description2: t('api.automation.feature1.description2', locale)
},
{
title: t('api.automation.feature2.title', locale),
description: t('api.automation.feature2.description', locale),
description2: t('api.automation.feature2.description2', locale)
},
{
title: t('api.automation.feature3.title', locale),
description: t('api.automation.feature3.description', locale)
}
]
</script>
<template>
<FeatureShowcaseSection
:heading="t('api.automation.heading', locale)"
:subtitle="t('api.automation.subtitle', locale)"
:features="features"
/>
</template>

View File

@@ -0,0 +1,124 @@
<script setup lang="ts">
import type { Locale } from '../../../i18n/translations'
import { externalLinks } from '../../../config/routes'
import { t } from '../../../i18n/translations'
import ProductHeroBadge from '../../common/ProductHeroBadge.vue'
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
</script>
<template>
<section class="overflow-hidden px-4 pt-20 pb-16 lg:px-20 lg:py-24">
<div class="mx-auto flex max-w-5xl flex-col items-center">
<div class="flex w-full max-w-2xl flex-col items-center text-center">
<ProductHeroBadge text="API" />
<h1
class="text-primary-comfy-canvas mt-8 text-5xl/tight font-light whitespace-pre-line lg:text-5xl"
>
{{ t('api.hero.heading', locale) }}
</h1>
<p
class="text-primary-comfy-canvas mt-8 max-w-xs text-sm lg:mt-20 lg:max-w-xl lg:text-base"
>
{{ t('api.hero.subtitle', locale) }}
</p>
<div
class="mt-10 flex w-full max-w-md flex-col gap-4 lg:w-auto lg:max-w-none lg:flex-row"
>
<a
:href="externalLinks.app"
target="_blank"
rel="noopener"
class="bg-primary-comfy-yellow text-primary-comfy-ink rounded-full px-8 py-4 text-center text-sm font-bold tracking-wider transition-opacity hover:opacity-90 lg:min-w-60"
>
{{ t('api.hero.getApiKeys', locale) }}
</a>
<a
:href="externalLinks.docs"
target="_blank"
rel="noopener"
class="border-primary-comfy-yellow text-primary-comfy-yellow hover:bg-primary-comfy-yellow hover:text-primary-comfy-ink rounded-full border px-8 py-4 text-center text-sm font-bold tracking-wider transition-colors lg:min-w-60"
>
{{ t('api.hero.viewDocs', locale) }}
</a>
</div>
</div>
<!-- Isometric node illustration -->
<div
class="relative mt-4 flex h-104 w-full max-w-4xl items-start justify-center overflow-hidden lg:mt-12 lg:h-136 lg:items-center"
>
<!-- Background layers -->
<div
class="border-secondary-mauve/20 absolute bottom-8 h-40 w-72 rounded-3xl border lg:bottom-16 lg:h-48 lg:w-96"
/>
<div
class="border-secondary-mauve/20 absolute bottom-16 h-40 w-60 rounded-3xl border lg:bottom-28 lg:h-48 lg:w-80"
/>
<div
class="border-secondary-mauve/20 absolute bottom-24 h-40 w-48 rounded-3xl border lg:bottom-40 lg:h-48 lg:w-64"
/>
<!-- Isometric grid of nodes -->
<div
class="relative z-10 mt-28 grid grid-cols-5 gap-1 lg:mt-0 lg:gap-1.5"
style="transform: rotateX(55deg) rotateZ(45deg)"
>
<span
class="bg-primary-comfy-yellow block size-8 rounded-lg lg:size-10"
/>
<span class="bg-secondary-mauve block size-8 rounded-lg lg:size-10" />
<span
class="bg-primary-comfy-plum block size-8 rounded-lg lg:size-10"
/>
<span class="bg-secondary-mauve block size-8 rounded-lg lg:size-10" />
<span
class="bg-primary-comfy-yellow block size-8 rounded-lg lg:size-10"
/>
<span class="bg-secondary-mauve block size-8 rounded-lg lg:size-10" />
<span
class="bg-primary-comfy-plum block size-8 rounded-lg lg:size-10"
/>
<span
class="bg-primary-comfy-yellow block size-8 rounded-lg lg:size-10"
/>
<span
class="bg-primary-comfy-plum block size-8 rounded-lg lg:size-10"
/>
<span class="bg-secondary-mauve block size-8 rounded-lg lg:size-10" />
<span
class="bg-primary-comfy-plum block size-8 rounded-lg lg:size-10"
/>
<span class="bg-secondary-mauve block size-8 rounded-lg lg:size-10" />
<span
class="bg-primary-comfy-plum block size-8 rounded-lg lg:size-10"
/>
<span class="bg-secondary-mauve block size-8 rounded-lg lg:size-10" />
<span
class="bg-primary-comfy-plum block size-8 rounded-lg lg:size-10"
/>
<span
class="bg-primary-comfy-yellow block size-8 rounded-lg lg:size-10"
/>
<span
class="bg-primary-comfy-plum block size-8 rounded-lg lg:size-10"
/>
<span class="bg-secondary-mauve block size-8 rounded-lg lg:size-10" />
<span
class="bg-primary-comfy-yellow block size-8 rounded-lg lg:size-10"
/>
<span
class="bg-primary-comfy-plum block size-8 rounded-lg lg:size-10"
/>
</div>
</div>
</div>
</section>
</template>

View File

@@ -0,0 +1,37 @@
<script setup lang="ts">
import type { Locale } from '../../../i18n/translations'
import { t } from '../../../i18n/translations'
import SharedReasonSection from '../shared/ReasonSection.vue'
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
const reasons = [
{
title: t('api.reason.1.title', locale),
description: t('api.reason.1.description', locale)
},
{
title: t('api.reason.2.title', locale),
description: t('api.reason.2.description', locale)
},
{
title: t('api.reason.3.title', locale),
description: t('api.reason.3.description', locale)
},
{
title: t('api.reason.4.title', locale),
description: t('api.reason.4.description', locale)
}
]
</script>
<template>
<SharedReasonSection
:heading="t('api.reason.heading', locale)"
:heading-highlight="t('api.reason.headingHighlight', locale)"
:heading-suffix="t('api.reason.headingSuffix', locale)"
:subtitle="t('api.reason.subtitle', locale)"
:reasons="reasons"
/>
</template>

View File

@@ -0,0 +1,93 @@
<script setup lang="ts">
import type { Locale } from '../../../i18n/translations'
import { externalLinks } from '../../../config/routes'
import { t } from '../../../i18n/translations'
import CardGridSection from '../shared/CardGridSection.vue'
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
const steps = [
{
number: '01',
titleKey: 'api.steps.step1.title' as const,
descriptionKey: 'api.steps.step1.description' as const
},
{
number: '02',
titleKey: 'api.steps.step2.title' as const,
descriptionKey: 'api.steps.step2.description' as const
},
{
number: '03',
titleKey: 'api.steps.step3.title' as const,
descriptionKey: 'api.steps.step3.description' as const
}
]
</script>
<template>
<CardGridSection :heading="t('api.steps.heading', locale)" :columns="3">
<div
v-for="step in steps"
:key="step.number"
class="bg-primary-comfy-ink flex aspect-square flex-col rounded-3xl border border-white/10 p-6"
>
<!-- Isometric illustration area -->
<div class="flex flex-1 items-center justify-center" aria-hidden="true">
<div
class="grid grid-cols-5 gap-0.5"
style="transform: rotateX(55deg) rotateZ(45deg)"
>
<span
v-for="i in 15"
:key="i"
class="block size-5 rounded-sm"
:class="[
i === 3 || i === 8 || i === 13
? 'bg-primary-comfy-yellow'
: i % 3 === 0
? 'bg-primary-comfy-plum'
: 'bg-secondary-mauve'
]"
/>
</div>
</div>
<!-- Step content -->
<p class="text-primary-comfy-yellow text-sm font-bold tracking-wider">
{{ step.number }}
</p>
<h3 class="text-primary-comfy-canvas mt-2 text-xl font-semibold">
{{ t(step.titleKey, locale) }}
</h3>
<p class="mt-3 text-sm text-smoke-700">
{{ t(step.descriptionKey, locale) }}
</p>
</div>
<!-- CTA buttons -->
<template #footer>
<div
class="mt-12 flex flex-col items-center gap-4 lg:flex-row lg:justify-center"
>
<a
:href="externalLinks.app"
target="_blank"
rel="noopener"
class="bg-primary-comfy-yellow text-primary-comfy-ink w-full rounded-full px-8 py-4 text-center text-sm font-bold tracking-wider transition-opacity hover:opacity-90 lg:w-auto lg:min-w-48"
>
{{ t('api.hero.getApiKeys', locale) }}
</a>
<a
:href="externalLinks.docs"
target="_blank"
rel="noopener"
class="border-primary-comfy-yellow text-primary-comfy-yellow hover:bg-primary-comfy-yellow hover:text-primary-comfy-ink w-full rounded-full border px-8 py-4 text-center text-sm font-bold tracking-wider transition-colors lg:w-auto lg:min-w-48"
>
{{ t('api.hero.viewDocs', locale) }}
</a>
</div>
</template>
</CardGridSection>
</template>

View File

@@ -0,0 +1,62 @@
<script setup lang="ts">
import type { Locale } from '../../../i18n/translations'
import { t } from '../../../i18n/translations'
import CardGridSection from '../shared/CardGridSection.vue'
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
const cards = [
{
titleKey: 'enterprise.byoKey.card1.title' as const,
descriptionKey: 'enterprise.byoKey.card1.description' as const
},
{
titleKey: 'enterprise.byoKey.card2.title' as const,
descriptionKey: 'enterprise.byoKey.card2.description' as const
}
]
</script>
<template>
<CardGridSection
:heading="t('enterprise.byoKey.heading', locale)"
:subtitle="t('enterprise.byoKey.subtitle', locale)"
:columns="2"
>
<div
v-for="(card, i) in cards"
:key="i"
class="bg-primary-comfy-ink flex aspect-square flex-col rounded-3xl border border-white/10 p-6"
>
<!-- Isometric illustration area -->
<div class="flex flex-1 items-center justify-center" aria-hidden="true">
<div
class="grid grid-cols-5 gap-0.5"
style="transform: rotateX(55deg) rotateZ(45deg)"
>
<span
v-for="j in 15"
:key="j"
class="block size-5 rounded-sm"
:class="[
j === 3 || j === 8 || j === 13
? 'bg-primary-comfy-yellow'
: j % 3 === 0
? 'bg-primary-comfy-plum'
: 'bg-secondary-mauve'
]"
/>
</div>
</div>
<!-- Card content -->
<h3 class="text-primary-comfy-canvas mt-2 text-xl font-semibold">
{{ t(card.titleKey, locale) }}
</h3>
<p class="mt-3 text-sm text-smoke-700">
{{ t(card.descriptionKey, locale) }}
</p>
</div>
</CardGridSection>
</template>

View File

@@ -0,0 +1,67 @@
<script setup lang="ts">
import type { Locale } from '../../../i18n/translations'
import { t } from '../../../i18n/translations'
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
</script>
<template>
<section class="relative overflow-hidden px-4 py-24 lg:px-20 lg:py-32">
<!-- Decorative images -->
<!-- Top-left -->
<div
class="absolute top-0 -left-8 size-36 rounded-3xl bg-smoke-200 lg:left-[10%] lg:size-64"
role="img"
aria-hidden="true"
/>
<!-- Top-right -->
<div
class="absolute top-0 -right-8 size-28 rounded-3xl bg-smoke-200 lg:top-4 lg:right-[2%] lg:size-40"
role="img"
aria-hidden="true"
/>
<!-- Middle-left -->
<div
class="absolute top-1/3 -left-12 size-40 rounded-3xl bg-smoke-200 lg:top-[35%] lg:left-[2%] lg:h-56 lg:w-48"
role="img"
aria-hidden="true"
/>
<!-- Middle-right -->
<div
class="absolute top-[45%] -right-4 hidden h-48 w-72 rounded-3xl bg-smoke-200 lg:block"
role="img"
aria-hidden="true"
/>
<!-- Bottom-right -->
<div
class="absolute -right-4 bottom-8 hidden size-48 rounded-3xl bg-smoke-200 lg:block"
role="img"
aria-hidden="true"
/>
<!-- Text content -->
<div
class="relative z-10 mx-auto flex max-w-3xl flex-col items-center py-16 text-center lg:py-24"
>
<h2 class="text-primary-comfy-canvas text-5xl font-light lg:text-8xl">
{{ t('enterprise.ownership.line1', locale) }}
</h2>
<h2
class="text-primary-comfy-canvas mt-6 text-5xl font-light lg:mt-10 lg:text-8xl"
>
{{ t('enterprise.ownership.line2', locale) }}
</h2>
<h2
class="text-primary-comfy-canvas mt-6 text-5xl font-light lg:mt-10 lg:text-8xl"
>
{{ t('enterprise.ownership.line3', locale) }}
</h2>
<p
class="text-primary-comfy-canvas mt-12 max-w-xl text-sm lg:mt-16 lg:text-base"
>
{{ t('enterprise.ownership.subtitle', locale) }}
</p>
</div>
</section>
</template>

View File

@@ -0,0 +1,122 @@
<script setup lang="ts">
import type { Locale } from '../../../i18n/translations'
import { t } from '../../../i18n/translations'
import ProductHeroBadge from '../../common/ProductHeroBadge.vue'
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
</script>
<template>
<section class="overflow-hidden px-4 pt-20 pb-16 lg:px-20 lg:py-24">
<div class="mx-auto flex max-w-5xl flex-col items-center">
<div class="flex w-full max-w-2xl flex-col items-center text-center">
<ProductHeroBadge text="ENTERPRISE" />
<h1
class="text-primary-comfy-canvas mt-8 text-5xl/tight font-light whitespace-pre-line lg:text-5xl"
>
{{ t('enterprise.hero.heading', locale) }}
</h1>
<p
class="text-primary-comfy-canvas mt-8 max-w-xs text-sm lg:mt-20 lg:max-w-xl lg:text-base"
>
{{ t('enterprise.hero.subtitle', locale) }}
</p>
<div
class="mt-10 flex w-full max-w-md flex-col lg:w-auto lg:max-w-none"
>
<a
href="/contact"
class="bg-primary-comfy-yellow text-primary-comfy-ink rounded-full px-8 py-4 text-center text-sm font-bold tracking-wider transition-opacity hover:opacity-90 lg:min-w-60"
>
{{ t('enterprise.hero.contactSales', locale) }}
</a>
</div>
</div>
<!-- Illustration -->
<div
class="relative mt-4 flex h-104 w-full max-w-4xl items-start justify-center overflow-hidden lg:mt-12 lg:h-136 lg:items-center"
>
<!-- Background hexagonal outlines -->
<div
class="border-secondary-mauve/20 absolute top-12 size-64 rounded-3xl border lg:top-8 lg:size-80"
/>
<div
class="border-secondary-mauve/20 absolute top-20 size-56 rounded-3xl border lg:top-16 lg:size-72"
/>
<!-- Isometric node clusters -->
<div class="relative z-10 mt-20 flex gap-4 lg:mt-0 lg:gap-6">
<div
class="grid grid-cols-3 gap-0.5"
style="transform: rotateX(55deg) rotateZ(45deg)"
>
<span
class="bg-secondary-mauve block size-7 rounded-lg lg:size-9"
/>
<span
class="bg-primary-comfy-plum block size-7 rounded-lg lg:size-9"
/>
<span
class="bg-primary-comfy-yellow block size-7 rounded-lg lg:size-9"
/>
<span
class="bg-primary-comfy-yellow block size-7 rounded-lg lg:size-9"
/>
<span
class="bg-primary-warm-gray block size-7 rounded-lg lg:size-9"
/>
<span
class="bg-secondary-mauve block size-7 rounded-lg lg:size-9"
/>
<span
class="bg-primary-comfy-plum block size-7 rounded-lg lg:size-9"
/>
<span
class="bg-primary-comfy-yellow block size-7 rounded-lg lg:size-9"
/>
<span
class="bg-secondary-mauve block size-7 rounded-lg lg:size-9"
/>
</div>
<div
class="grid grid-cols-3 gap-0.5"
style="transform: rotateX(55deg) rotateZ(45deg)"
>
<span
class="bg-primary-warm-gray block size-7 rounded-lg lg:size-9"
/>
<span
class="bg-primary-comfy-yellow block size-7 rounded-lg lg:size-9"
/>
<span
class="bg-secondary-mauve block size-7 rounded-lg lg:size-9"
/>
<span
class="bg-secondary-mauve block size-7 rounded-lg lg:size-9"
/>
<span
class="bg-primary-comfy-plum block size-7 rounded-lg lg:size-9"
/>
<span
class="bg-primary-warm-gray block size-7 rounded-lg lg:size-9"
/>
<span
class="bg-primary-comfy-yellow block size-7 rounded-lg lg:size-9"
/>
<span
class="bg-secondary-mauve block size-7 rounded-lg lg:size-9"
/>
<span
class="bg-primary-comfy-plum block size-7 rounded-lg lg:size-9"
/>
</div>
</div>
</div>
</div>
</section>
</template>

View File

@@ -0,0 +1,61 @@
<script setup lang="ts">
import type { Locale } from '../../../i18n/translations'
import { t } from '../../../i18n/translations'
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
</script>
<template>
<section class="px-4 py-24 lg:px-20">
<div
class="bg-transparency-white-t4 rounded-5xl flex flex-col gap-8 p-2 lg:flex-row lg:items-stretch lg:gap-16"
>
<!-- Image -->
<div class="lg:w-1/2">
<div
class="aspect-square w-full rounded-4xl bg-smoke-200"
role="img"
:aria-label="t('enterprise.orchestration.heading', locale)"
/>
</div>
<!-- Text -->
<div class="flex flex-col justify-between p-6 lg:w-1/2">
<div>
<h2
class="text-primary-comfy-canvas text-3xl font-light lg:text-5xl/tight"
>
{{ t('enterprise.orchestration.heading', locale) }}
</h2>
<p
class="text-primary-comfy-yellow mt-8 text-lg font-semibold lg:mt-10 lg:text-2xl"
>
{{ t('enterprise.orchestration.highlight', locale) }}
</p>
<p class="mt-6 text-sm text-smoke-700 lg:text-base">
{{ t('enterprise.orchestration.description', locale) }}
</p>
<p class="mt-4 text-sm text-smoke-500 italic lg:text-base">
{{ t('enterprise.orchestration.quote', locale) }}
</p>
</div>
<div class="mt-10 lg:mt-0">
<a
href="/contact"
class="bg-primary-comfy-yellow text-primary-comfy-ink inline-block rounded-full px-8 py-4 text-sm font-bold tracking-wider transition-opacity hover:opacity-90"
>
{{ t('enterprise.hero.contactSales', locale) }}
</a>
<p class="mt-4 text-xs text-smoke-500 lg:text-sm">
{{ t('enterprise.orchestration.footer', locale) }}
</p>
</div>
</div>
</div>
</section>
</template>

View File

@@ -0,0 +1,39 @@
<script setup lang="ts">
import type { Locale } from '../../../i18n/translations'
import { t } from '../../../i18n/translations'
import SharedReasonSection from '../shared/ReasonSection.vue'
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
const reasons = [
{
title: t('enterprise.reason.1.title', locale),
description: t('enterprise.reason.1.description', locale)
},
{
title: t('enterprise.reason.2.title', locale),
description: t('enterprise.reason.2.description', locale)
},
{
title: t('enterprise.reason.3.title', locale),
description: t('enterprise.reason.3.description', locale)
},
{
title: t('enterprise.reason.4.title', locale),
description: t('enterprise.reason.4.description', locale)
},
{
title: t('enterprise.reason.5.title', locale),
description: t('enterprise.reason.5.description', locale)
}
]
</script>
<template>
<SharedReasonSection
:heading="t('enterprise.reason.heading', locale)"
:subtitle="t('enterprise.reason.subtitle', locale)"
:reasons="reasons"
/>
</template>

View File

@@ -0,0 +1,38 @@
<script setup lang="ts">
import type { Locale } from '../../../i18n/translations'
import { getRoutes } from '../../../config/routes'
import { t } from '../../../i18n/translations'
import FeatureShowcaseSection from '../shared/FeatureShowcaseSection.vue'
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
const routes = getRoutes(locale)
const features = [
{
title: t('enterprise.team.feature1.title', locale),
description: t('enterprise.team.feature1.description', locale)
},
{
title: t('enterprise.team.feature2.title', locale),
description: t('enterprise.team.feature2.description', locale),
ctaText: t('enterprise.team.feature2.cta', locale),
ctaHref: routes.cloud
},
{
title: t('enterprise.team.feature3.title', locale),
description: t('enterprise.team.feature3.description', locale),
ctaText: t('enterprise.hero.contactSales', locale),
ctaHref: '/contact'
}
]
</script>
<template>
<FeatureShowcaseSection
:heading="t('enterprise.team.heading', locale)"
:subtitle="t('enterprise.team.subtitle', locale)"
:features="features"
/>
</template>

View File

@@ -0,0 +1,69 @@
<script setup lang="ts">
import type { Locale } from '../../../i18n/translations'
import { useDownloadUrl } from '../../../composables/useDownloadUrl'
import { externalLinks } from '../../../config/routes'
import { t } from '../../../i18n/translations'
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
const { downloadUrl } = useDownloadUrl()
</script>
<template>
<section class="px-4 py-24 lg:px-20 lg:py-40">
<div
class="flex flex-col-reverse items-stretch gap-10 lg:flex-row lg:gap-16"
>
<!-- Text content -->
<div class="flex flex-1 flex-col justify-between">
<div>
<h2 class="text-primary-comfy-canvas text-3xl font-light lg:text-4xl">
{{ t('download.ecosystem.heading', locale) }}
</h2>
<p class="text-primary-comfy-canvas/70 mt-6 text-sm">
{{ t('download.ecosystem.description', locale) }}
</p>
</div>
<!-- CTA buttons -->
<div class="mt-10 flex flex-col gap-4 lg:flex-row">
<a
:href="downloadUrl"
target="_blank"
rel="noopener"
class="bg-primary-comfy-yellow text-primary-comfy-ink rounded-full px-8 py-4 text-center text-sm font-bold tracking-wider transition-opacity hover:opacity-90"
>
{{ t('download.hero.downloadLocal', locale) }}
</a>
<a
:href="externalLinks.github"
target="_blank"
rel="noopener"
class="border-primary-comfy-yellow text-primary-comfy-yellow hover:bg-primary-comfy-yellow hover:text-primary-comfy-ink flex items-center justify-center gap-2 rounded-full border px-8 py-4 text-sm font-bold tracking-wider transition-colors"
>
<span
class="icon-mask size-5 mask-[url('/icons/social/github.svg')]"
aria-hidden="true"
/>
{{ t('download.hero.installGithub', locale) }}
</a>
</div>
</div>
<!-- TODO: Replace with final ecosystem illustration -->
<div class="flex-1">
<div
class="aspect-4/3 w-full overflow-hidden rounded-3xl bg-linear-to-b from-emerald-300 to-amber-200"
>
<div class="flex h-full items-center justify-center">
<span
class="bg-primary-comfy-ink text-primary-comfy-yellow rounded-full px-4 py-2 text-sm font-bold"
>
4x
</span>
</div>
</div>
</div>
</div>
</section>
</template>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import type { Locale } from '../../../i18n/translations'
import FAQSection from '../../common/FAQSection.vue'
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
</script>
<template>
<FAQSection
:locale="locale"
heading-key="download.faq.heading"
faq-prefix="download.faq"
:faq-count="8"
/>
</template>

View File

@@ -0,0 +1,85 @@
<script setup lang="ts">
import type { Locale } from '../../../i18n/translations'
import { useDownloadUrl } from '../../../composables/useDownloadUrl'
import { externalLinks } from '../../../config/routes'
import { t } from '../../../i18n/translations'
import ProductHeroBadge from '../../common/ProductHeroBadge.vue'
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
const { downloadUrl } = useDownloadUrl()
</script>
<template>
<section class="overflow-hidden px-4 pt-20 pb-16 lg:px-20 lg:py-24">
<div class="mx-auto flex max-w-5xl flex-col items-center">
<div class="flex w-full max-w-2xl flex-col items-center text-center">
<ProductHeroBadge />
<h1
class="text-primary-comfy-canvas mt-8 max-w-[10ch] text-5xl/tight font-light whitespace-pre-line lg:max-w-none lg:text-5xl"
>
{{ t('download.hero.heading', locale) }}
</h1>
<p
class="text-primary-comfy-canvas mt-8 max-w-xs text-sm lg:mt-20 lg:max-w-xl lg:text-base"
>
{{ t('download.hero.subtitle', locale) }}
</p>
<div
class="mt-10 flex w-full max-w-md flex-col gap-4 lg:w-auto lg:max-w-none lg:flex-row"
>
<a
:href="downloadUrl"
target="_blank"
rel="noopener"
class="bg-primary-comfy-yellow text-primary-comfy-ink rounded-full px-8 py-4 text-center text-sm font-bold tracking-wider transition-opacity hover:opacity-90 lg:min-w-60"
>
{{ t('download.hero.downloadLocal', locale) }}
</a>
<a
:href="externalLinks.github"
target="_blank"
rel="noopener"
class="border-primary-comfy-yellow text-primary-comfy-yellow hover:bg-primary-comfy-yellow hover:text-primary-comfy-ink flex items-center justify-center gap-2 rounded-full border px-8 py-4 text-sm font-bold tracking-wider transition-colors lg:min-w-60"
>
<span
class="icon-mask size-5 mask-[url('/icons/social/github.svg')]"
aria-hidden="true"
/>
{{ t('download.hero.installGithub', locale) }}
</a>
</div>
</div>
<!-- Placeholder for future animation; clipped within hero section -->
<div
class="relative mt-4 flex h-104 w-full max-w-4xl items-start justify-center overflow-hidden lg:mt-12 lg:h-136 lg:items-center"
>
<div
class="border-secondary-mauve/35 bg-primary-comfy-ink/35 absolute top-20 left-2 h-64 w-44 rotate-30 rounded-4xl border"
/>
<div
class="border-secondary-mauve/35 bg-primary-comfy-ink/35 absolute top-28 left-20 h-56 w-40 rotate-30 rounded-4xl border"
/>
<div
class="border-secondary-mauve/35 bg-primary-comfy-ink/35 absolute top-52 right-4 h-56 w-40 rotate-30 rounded-4xl border"
/>
<div class="relative z-10 mt-28 grid grid-cols-3 gap-0.5 lg:mt-0">
<span class="bg-secondary-mauve block size-9 rounded-lg" />
<span class="bg-primary-comfy-plum block size-9 rounded-lg" />
<span class="bg-secondary-mauve block size-9 rounded-lg" />
<span class="bg-primary-comfy-plum block size-9 rounded-lg" />
<span class="bg-primary-comfy-yellow block size-9 rounded-lg" />
<span class="bg-secondary-mauve block size-9 rounded-lg" />
<span class="bg-secondary-mauve block size-9 rounded-lg" />
<span class="bg-primary-comfy-plum block size-9 rounded-lg" />
<span class="bg-secondary-mauve block size-9 rounded-lg" />
</div>
</div>
</div>
</section>
</template>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { Locale } from '../../../i18n/translations'
import ProductCardsSection from '../../common/ProductCardsSection.vue'
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
</script>
<template>
<ProductCardsSection
:locale="locale"
exclude-product="local"
label-key="products.labelProducts"
/>
</template>

View File

@@ -0,0 +1,35 @@
<script setup lang="ts">
import type { Locale } from '../../../i18n/translations'
import { t } from '../../../i18n/translations'
import SharedReasonSection from '../shared/ReasonSection.vue'
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
const reasons = [
{
title: t('download.reason.1.title', locale),
description: t('download.reason.1.description', locale)
},
{
title: t('download.reason.2.title', locale),
description: t('download.reason.2.description', locale)
},
{
title: t('download.reason.3.title', locale),
description: t('download.reason.3.description', locale)
},
{
title: t('download.reason.4.title', locale),
description: t('download.reason.4.description', locale)
}
]
</script>
<template>
<SharedReasonSection
:heading="t('download.reason.heading', locale)"
:heading-highlight="t('download.reason.headingHighlight', locale)"
:reasons="reasons"
/>
</template>

View File

@@ -0,0 +1,31 @@
<script setup lang="ts">
defineProps<{
heading: string
subtitle?: string
columns?: 2 | 3
}>()
</script>
<template>
<section class="px-4 py-24 lg:px-20">
<div class="mx-auto max-w-3xl text-center">
<h2
class="text-primary-comfy-canvas text-3xl font-light lg:text-5xl/tight"
>
{{ heading }}
</h2>
<p v-if="subtitle" class="mt-4 text-sm text-smoke-700 lg:text-base">
{{ subtitle }}
</p>
</div>
<div
class="mt-12 grid gap-6 lg:mt-16"
:class="columns === 2 ? 'lg:grid-cols-2' : 'lg:grid-cols-3'"
>
<slot />
</div>
<slot name="footer" />
</section>
</template>

View File

@@ -0,0 +1,29 @@
<script setup lang="ts">
import type { Locale } from '../../../i18n/translations'
import { externalLinks } from '../../../config/routes'
import { t } from '../../../i18n/translations'
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
</script>
<template>
<section
class="bg-transparency-white-t4 px-4 pt-28 pb-4 text-center lg:px-20 lg:pt-36 lg:pb-8"
>
<p
class="text-primary-comfy-canvas text-lg font-semibold lg:text-sm lg:font-normal"
>
{{ t('download.cloud.prefix', locale) }}
<a
:href="externalLinks.app"
class="text-primary-comfy-yellow mx-1 font-bold tracking-widest uppercase hover:underline"
>
{{ t('download.cloud.cta', locale) }}
</a>
<span class="mt-1 block lg:mt-0 lg:inline">
{{ t('download.cloud.suffix', locale) }}
</span>
</p>
</section>
</template>

View File

@@ -0,0 +1,87 @@
<script setup lang="ts">
import { cn } from '@comfyorg/tailwind-utils'
interface Feature {
title: string
description: string
description2?: string
ctaText?: string
ctaHref?: string
}
defineProps<{
heading: string
subtitle: string
features: Feature[]
}>()
</script>
<template>
<section class="px-4 py-24 lg:px-20">
<!-- Section header -->
<div class="mx-auto max-w-3xl text-center">
<h2
class="text-primary-comfy-canvas text-3xl font-light lg:text-5xl/tight"
>
{{ heading }}
</h2>
<p class="mt-4 text-sm text-smoke-700 lg:text-base">
{{ subtitle }}
</p>
</div>
<!-- Features -->
<div class="mt-24 flex flex-col gap-4 lg:gap-8">
<div
v-for="(feature, i) in features"
:key="i"
class="bg-transparency-white-t4 rounded-5xl flex flex-col gap-8 p-2 lg:flex-row lg:items-stretch lg:gap-12"
>
<!-- Text -->
<div
:class="
cn(
'order-2 flex flex-col p-6 lg:w-1/2 lg:justify-between',
i % 2 === 0 ? 'lg:order-1' : 'lg:order-2'
)
"
>
<h3 class="text-primary-comfy-canvas text-2xl font-light lg:text-3xl">
{{ feature.title }}
</h3>
<div class="mt-6 lg:mt-0">
<p class="text-sm text-smoke-700 lg:text-base">
{{ feature.description }}
</p>
<p
v-if="feature.description2"
class="mt-4 text-sm text-smoke-700 lg:text-base"
>
{{ feature.description2 }}
</p>
<a
v-if="feature.ctaText && feature.ctaHref"
:href="feature.ctaHref"
class="bg-primary-comfy-yellow text-primary-comfy-ink mt-6 inline-block rounded-full px-6 py-3 text-xs font-bold tracking-wider transition-opacity hover:opacity-90"
>
{{ feature.ctaText }}
</a>
</div>
</div>
<!-- Image -->
<div
:class="
cn('order-1 lg:w-1/2', i % 2 === 0 ? 'lg:order-2' : 'lg:order-1')
"
>
<div
class="aspect-4/3 w-full rounded-4xl bg-smoke-200"
role="img"
:aria-label="feature.title"
/>
</div>
</div>
</div>
</section>
</template>

View File

@@ -0,0 +1,62 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import ReasonSection from './ReasonSection.vue'
const meta: Meta<typeof ReasonSection> = {
title: 'Website/Product/ReasonSection',
component: ReasonSection,
tags: ['autodocs'],
decorators: [
() => ({
template: '<div class="bg-primary-comfy-ink p-8"><story /></div>'
})
],
args: {
heading: 'Why professionals\nchoose ',
headingHighlight: 'Comfy Local',
reasons: [
{
title: 'Unlimited\ncreative power',
description:
'Run any workflow without limits. No queues, no credits, no restrictions on what you can create.'
},
{
title: 'Any model,\nany time',
description:
'Use any open-source model instantly. Switch between Stable Diffusion, Flux, and more with a single click.'
},
{
title: 'Your machine,\nyour rules',
description:
'Your data never leaves your computer. Full privacy and complete control over your creative environment.'
},
{
title: 'Free.\nOpen Source.',
description:
'No subscriptions, no hidden fees. ComfyUI is and always will be free and open source.'
}
]
}
}
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {}
export const WithoutHighlight: Story = {
args: {
heading: 'Why choose Comfy',
headingHighlight: '',
reasons: [
{
title: 'Fast',
description: 'Optimized for speed and efficiency.'
},
{
title: 'Flexible',
description: 'Adapt to any workflow with ease.'
}
]
}
}

View File

@@ -0,0 +1,62 @@
<script setup lang="ts">
interface Reason {
title: string
description: string
}
const {
heading,
headingHighlight = '',
headingSuffix = '',
subtitle = '',
reasons
} = defineProps<{
heading: string
headingHighlight?: string
headingSuffix?: string
subtitle?: string
reasons: Reason[]
}>()
</script>
<template>
<section
class="flex flex-col gap-4 px-4 py-24 md:flex-row md:gap-16 md:px-20 md:py-40"
>
<!-- Left heading -->
<div
class="bg-primary-comfy-ink sticky top-20 z-10 w-full shrink-0 self-start py-4 md:top-28 md:w-115 md:py-0"
>
<h2
class="text-primary-comfy-canvas text-4xl font-light whitespace-pre-line md:text-5xl"
>
{{ heading
}}<span v-if="headingHighlight" class="text-primary-warm-white">{{
headingHighlight
}}</span
>{{ headingSuffix }}
</h2>
<p v-if="subtitle" class="text-primary-comfy-canvas/70 mt-6 text-sm">
{{ subtitle }}
</p>
</div>
<!-- Right reasons list -->
<div class="flex-1">
<div
v-for="(reason, i) in reasons"
:key="i"
class="border-primary-comfy-canvas/20 flex flex-col gap-4 border-b py-10 first:pt-0 md:flex-row md:gap-12"
>
<h3
class="text-primary-comfy-canvas shrink-0 text-2xl font-light whitespace-pre-line md:w-52"
>
{{ reason.title }}
</h3>
<p class="text-primary-comfy-canvas/70 flex-1 text-sm">
{{ reason.description }}
</p>
</div>
</div>
</section>
</template>

View File

@@ -0,0 +1,28 @@
import { externalLinks } from '@/config/routes'
const downloadUrls = {
windows: 'https://download.comfy.org/windows/nsis/x64',
macArm: 'https://download.comfy.org/mac/dmg/arm64'
} as const
function isMobile(ua: string): boolean {
return /iphone|ipad|ipod|android/.test(ua)
}
// TODO: Only Windows x64 and macOS arm64 are available today.
// When Linux and/or macIntel builds are added, extend detection and URLs here.
function getDownloadUrl(): string {
if (typeof navigator === 'undefined') return externalLinks.github
const ua = navigator.userAgent.toLowerCase()
if (isMobile(ua)) return externalLinks.github
if (ua.includes('win')) return downloadUrls.windows
if (ua.includes('macintosh') || ua.includes('mac os x'))
return downloadUrls.macArm
return externalLinks.github
}
export function useDownloadUrl() {
return { downloadUrl: getDownloadUrl() }
}

View File

@@ -59,8 +59,9 @@ export function usePinScrub(refs: PinScrubRefs, options: PinScrubOptions) {
function cacheLayout() {
const contentRect = content.getBoundingClientRect()
const sectionStyle = getComputedStyle(section)
contentH = content.scrollHeight
vpH = window.innerHeight
vpH = window.innerHeight - parseFloat(sectionStyle.paddingTop)
buttonCenters = Array.from(nav.querySelectorAll(':scope > *')).map(
(btn) => {
const btnRect = btn.getBoundingClientRect()

View File

@@ -109,7 +109,7 @@ const translations = {
'zh-CN': '启动云端'
},
'getStarted.step1.or': {
en: 'or',
en: ' or ',
'zh-CN': '或'
},
'getStarted.step2.title': {
@@ -134,6 +134,10 @@ const translations = {
en: 'Comfy UI',
'zh-CN': 'Comfy UI'
},
'products.labelProducts': {
en: 'Products',
'zh-CN': '产品'
},
'products.heading': {
en: 'The AI creation\nengine for complete control',
'zh-CN': '完全掌控的\nAI 创作引擎'
@@ -218,6 +222,498 @@ const translations = {
en: "Comfy gives you the building blocks to create workflows nobody's imagined yet — and share them with everyone.",
'zh-CN': 'Comfy 为您提供构建模块,创造出前所未有的工作流——并与所有人分享。'
},
// API HeroSection
'api.hero.heading': {
en: 'Turn any workflow into\na production endpoint.',
'zh-CN': '将任何工作流转化为\n生产级端点。'
},
'api.hero.subtitle': {
en: 'Design your workflows. Deploy them as API calls. Automate generation, integrate with your systems, and scale to thousands of outputs on Comfy Cloud.',
'zh-CN':
'设计你的工作流。将它们部署为 API 调用。自动化生成、与你的系统集成,并在 Comfy Cloud 上扩展到数千个输出。'
},
'api.hero.getApiKeys': {
en: 'GET API KEYS',
'zh-CN': '获取 API 密钥'
},
'api.hero.viewDocs': {
en: 'VIEW DOCS',
'zh-CN': '查看文档'
},
// Enterprise TeamSection
'enterprise.team.heading': {
en: 'Team workspaces\nand shared assets.',
'zh-CN': '团队工作区\n与共享资产。'
},
'enterprise.team.subtitle': {
en: 'Organize workflows, models, and outputs in shared workspaces. Control who builds, who runs, and who deploys.',
'zh-CN':
'在共享工作区中组织工作流、模型和输出。控制谁构建、谁运行、谁部署。'
},
'enterprise.team.feature1.title': {
en: 'Role-based access',
'zh-CN': '基于角色的访问控制'
},
'enterprise.team.feature1.description': {
en: 'Control who builds, who runs, and who deploys.',
'zh-CN': '控制谁构建、谁运行、谁部署。'
},
'enterprise.team.feature2.title': {
en: 'Single Sign-On',
'zh-CN': '单点登录'
},
'enterprise.team.feature2.description': {
en: 'Most things you build locally run on Comfy Cloud \u2014 same file, same results, powerful GPUs on demand. When a job outgrows your machine, push it to the cloud. No conversion, no rework.',
'zh-CN':
'你在本地构建的大部分内容都能在 Comfy Cloud 上运行——相同文件、相同结果、按需使用强大 GPU。当任务超出你的机器能力时推送到云端。无需转换无需返工。'
},
'enterprise.team.feature2.cta': {
en: 'SEE CLOUD FEATURES',
'zh-CN': '查看云端特性'
},
'enterprise.team.feature3.title': {
en: 'App Mode',
'zh-CN': 'App 模式'
},
'enterprise.team.feature3.description': {
en: 'Non-technical team members run workflows without touching the node graph.',
'zh-CN': '非技术团队成员无需接触节点图即可运行工作流。'
},
// Enterprise ReasonSection
'enterprise.reason.heading': {
en: 'Enterprise-grade infrastructure for the creative engine inside your organization.',
'zh-CN': '为组织内的创作引擎提供企业级基础设施。'
},
'enterprise.reason.subtitle': {
en: 'Comfy Cloud API gives you a managed infrastructure so you can focus on the workflow, not the hardware.',
'zh-CN':
'Comfy Cloud API 为你提供托管基础设施,让你专注于工作流,而非硬件。'
},
'enterprise.reason.1.title': {
en: 'Dedicated GPU compute',
'zh-CN': '专属 GPU 算力'
},
'enterprise.reason.1.description': {
en: 'Reserved server-grade, powerful compute for your organization.',
'zh-CN': '为你的组织预留的服务器级强大算力。'
},
'enterprise.reason.2.title': {
en: 'Priority queuing',
'zh-CN': '优先队列'
},
'enterprise.reason.2.description': {
en: 'Your production jobs run first.',
'zh-CN': '你的生产任务优先运行。'
},
'enterprise.reason.3.title': {
en: 'Flexible Deployment',
'zh-CN': '灵活部署'
},
'enterprise.reason.3.description': {
en: 'Same workflows on Comfy Cloud. Scale compute up or down based on your production schedule.',
'zh-CN': '在 Comfy Cloud 上运行相同的工作流。根据生产计划弹性扩缩算力。'
},
'enterprise.reason.4.title': {
en: 'Custom SLAs',
'zh-CN': '自定义 SLA'
},
'enterprise.reason.4.description': {
en: 'Uptime and response commitments built around your schedule.',
'zh-CN': '根据你的时间安排定制正常运行时间和响应承诺。'
},
'enterprise.reason.5.title': {
en: 'Commercial license guaranteed',
'zh-CN': '商业许可保障'
},
'enterprise.reason.5.description': {
en: 'Every model available through Comfy Cloud is cleared for commercial use. No license ambiguity for your legal team.',
'zh-CN':
'通过 Comfy Cloud 提供的每个模型均已获得商业使用许可。法务团队无需担心许可歧义。'
},
// Enterprise HeroSection
'enterprise.hero.heading': {
en: 'Your team already runs ComfyUI. Scale it with confidence.',
'zh-CN': '你的团队已经在使用 ComfyUI。放心地扩展它。'
},
'enterprise.hero.subtitle': {
en: 'ComfyUI Enterprise adds managed infrastructure, team controls, and dedicated support to the workflows your organization already builds.',
'zh-CN':
'ComfyUI 企业版为你的组织已有的工作流添加托管基础设施、团队控制和专属支持。'
},
'enterprise.hero.contactSales': {
en: 'CONTACT SALES',
'zh-CN': '联系销售'
},
// Enterprise DataOwnershipSection
'enterprise.ownership.line1': {
en: 'Your data.',
'zh-CN': '你的数据。'
},
'enterprise.ownership.line2': {
en: 'Your network.',
'zh-CN': '你的网络。'
},
'enterprise.ownership.line3': {
en: 'Your terms.',
'zh-CN': '你的条款。'
},
'enterprise.ownership.subtitle': {
en: 'Your workflows, models, and generated outputs stay within your organization\u2019s environment. Role-based access controls and data isolation built for organizations with the strictest requirements.',
'zh-CN':
'你的工作流、模型和生成输出始终保留在你的组织环境中。基于角色的访问控制和数据隔离,为最严格要求的组织而构建。'
},
// Enterprise BYOKeySection
'enterprise.byoKey.heading': {
en: 'Bring your own API key',
'zh-CN': '自带 API 密钥'
},
'enterprise.byoKey.subtitle': {
en: 'Use your own contracts with third-party model providers. Comfy orchestrates the pipeline. You choose which models to run and whose API keys to use.',
'zh-CN':
'使用你与第三方模型提供商的合约。Comfy 编排管线。你决定运行哪些模型、使用谁的 API 密钥。'
},
'enterprise.byoKey.card1.title': {
en: 'API key management',
'zh-CN': 'API 密钥管理'
},
'enterprise.byoKey.card1.description': {
en: 'Bring your own API keys from any model provider. Use your existing contracts and pricing.',
'zh-CN': '从任何模型提供商导入你自己的 API 密钥。使用你现有的合约和定价。'
},
'enterprise.byoKey.card2.title': {
en: 'Real-time progress',
'zh-CN': '实时进度'
},
'enterprise.byoKey.card2.description': {
en: 'Step-by-step execution updates via WebSocket.',
'zh-CN': '通过 WebSocket 逐步更新执行状态。'
},
// Enterprise OrchestrationSection
'enterprise.orchestration.heading': {
en: 'The orchestration layer isn\u2019t worth rebuilding.',
'zh-CN': '编排层不值得重建。'
},
'enterprise.orchestration.highlight': {
en: 'Every team that evaluates building this internally reaches the same conclusion.',
'zh-CN': '每个评估过内部自建的团队都得出了同样的结论。'
},
'enterprise.orchestration.description': {
en: 'Internal estimates are 12\u201318 months with a dedicated engineering team. No matter how good or how fast, building this internally won\u2019t have 5,000+ extensions, 60,000+ nodes, or same-day model support.',
'zh-CN':
'内部评估需要一个专属工程团队耗时 12\u201318 个月。无论多优秀多快速,自建方案都不会拥有 5,000+ 扩展、60,000+ 节点或当日模型支持。'
},
'enterprise.orchestration.quote': {
en: 'Platforms like OpenArt and Fal already run Comfy underneath \u2014 because they reached the same conclusion.',
'zh-CN':
'OpenArt 和 Fal 等平台底层已经运行 Comfy——因为他们得出了同样的结论。'
},
'enterprise.orchestration.footer': {
en: 'Comfy Enterprise plans come with support from the team that builds the engine: direct access to our engineering team and a whiteglove onboarding.',
'zh-CN':
'Comfy 企业版计划附带引擎开发团队的支持:直接访问我们的工程团队和白手套式入职服务。'
},
// API StepsSection
'api.steps.heading': {
en: 'Three steps to production',
'zh-CN': '三步进入生产'
},
'api.steps.step1.title': {
en: 'Design the workflow',
'zh-CN': '设计工作流'
},
'api.steps.step1.description': {
en: 'Build and test in the ComfyUI interface \u2014 locally or on Comfy Cloud. When it works, export the API format.',
'zh-CN':
'在 ComfyUI 界面中构建和测试——本地或 Comfy Cloud 上。测试通过后,导出 API 格式。'
},
'api.steps.step2.title': {
en: 'Submit via API',
'zh-CN': '通过 API 提交'
},
'api.steps.step2.description': {
en: 'Send the workflow JSON or a single request to the API endpoint. Modify inputs programmatically \u2014 prompts, seeds, images, any parameter on any node.',
'zh-CN':
'将工作流 JSON 或单个请求发送到 API 端点。以编程方式修改输入——提示词、种子、图像、任何节点上的任何参数。'
},
'api.steps.step3.title': {
en: 'Monitor and Retrieve',
'zh-CN': '监控与获取'
},
'api.steps.step3.description': {
en: 'Track progress in real time over WebSocket. Download the finished output when the job completes.',
'zh-CN': '通过 WebSocket 实时跟踪进度。作业完成后下载最终输出。'
},
// API AutomationSection
'api.automation.heading': {
en: 'The automation layer for\nprofessional-grade visual AI',
'zh-CN': '专业级视觉 AI 的\n自动化层'
},
'api.automation.subtitle': {
en: 'To transform ComfyUI from a powerful tool to reliable infrastructure.',
'zh-CN': '将 ComfyUI 从强大的工具转化为可靠的基础设施。'
},
'api.automation.feature1.title': {
en: "Automate what can't be manual",
'zh-CN': '自动化无法手动完成的工作'
},
'api.automation.feature1.description': {
en: 'Trigger generation when a user uploads a photo. Process an entire product catalog overnight. Swap a character\u2019s expression across 500 frames.',
'zh-CN':
'当用户上传照片时触发生成。一夜之间处理整个产品目录。在 500 帧中替换角色表情。'
},
'api.automation.feature1.description2': {
en: 'The API is for running workflows inside other systems \u2014 triggered by code, not by a person at a keyboard.',
'zh-CN':
'API 用于在其他系统内运行工作流——由代码触发,而非由键盘前的人触发。'
},
'api.automation.feature2.title': {
en: 'Inject dynamic values at runtime',
'zh-CN': '在运行时注入动态值'
},
'api.automation.feature2.description': {
en: 'Pass a different face, a different product, a different brand palette into the same pipeline \u2014 consistent, controlled output every time.',
'zh-CN':
'将不同的面孔、不同的产品、不同的品牌色板传入同一管线——每次都获得一致、可控的输出。'
},
'api.automation.feature2.description2': {
en: 'The workflow stays fixed. The inputs change.',
'zh-CN': '工作流保持不变。输入可以改变。'
},
'api.automation.feature3.title': {
en: 'Integrate ComfyUI into the rest of your stack',
'zh-CN': '将 ComfyUI 集成到你的技术栈中'
},
'api.automation.feature3.description': {
en: 'Generation is usually one step in a larger system. Fetch input from S3, call the ComfyUI API, post-process the result, write it to a database, and notify a team.',
'zh-CN':
'生成通常只是更大系统中的一个步骤。从 S3 获取输入、调用 ComfyUI API、后处理结果、写入数据库、通知团队。'
},
// API ReasonSection
'api.reason.heading': {
en: 'Deploy on\n',
'zh-CN': '部署在\n'
},
'api.reason.headingHighlight': {
en: 'Comfy Cloud —\n',
'zh-CN': 'Comfy Cloud——\n'
},
'api.reason.headingSuffix': {
en: 'no servers to\nmanage.',
'zh-CN': '无需管理\n服务器。'
},
'api.reason.subtitle': {
en: 'Comfy Cloud API gives you a managed infrastructure so you can focus on the workflow, not the hardware.',
'zh-CN':
'Comfy Cloud API 为你提供托管基础设施,让你专注于工作流,而非硬件。'
},
'api.reason.1.title': {
en: 'Powerful GPUs on demand',
'zh-CN': '按需使用强大 GPU'
},
'api.reason.1.description': {
en: 'Heavy video generation, complex multi-step pipelines, large batch jobs.',
'zh-CN': '重度视频生成、复杂多步管线、大批量任务。'
},
'api.reason.2.title': {
en: 'One API key. Immediate access.',
'zh-CN': '一个 API 密钥。即时访问。'
},
'api.reason.2.description': {
en: 'No environment setup, no model downloads, no GPU procurement.',
'zh-CN': '无需环境配置、无需下载模型、无需采购 GPU。'
},
'api.reason.3.title': {
en: 'Open models & partner models through one endpoint',
'zh-CN': '通过一个端点访问开源模型和合作伙伴模型'
},
'api.reason.3.description': {
en: 'Open-source models alongside partner models like Kling, Luma, Nano Banana, Grok, and Runway \u2014 all with one credit balance.',
'zh-CN':
'开源模型与 Kling、Luma、Nano Banana、Grok、Runway 等合作伙伴模型并存——全部使用同一额度。'
},
'api.reason.4.title': {
en: 'Real-time progress',
'zh-CN': '实时进度'
},
'api.reason.4.description': {
en: 'Step-by-step execution updates and live previews during generation via WebSocket.',
'zh-CN': '通过 WebSocket 获取逐步执行更新和生成过程中的实时预览。'
},
// Download FAQSection
'download.faq.heading': {
en: "FAQ's",
'zh-CN': '常见问题'
},
'download.faq.1.q': {
en: 'Do I need a GPU to run ComfyUI locally?',
'zh-CN': '本地运行 ComfyUI 需要 GPU 吗?'
},
'download.faq.1.a': {
en: 'A dedicated GPU is strongly recommended — more VRAM means bigger models and batches. No GPU? Run the same workflow on Comfy Cloud.',
'zh-CN':
'强烈建议使用独立 GPU——更大的显存意味着更大的模型和批量。没有 GPU在 Comfy Cloud 上运行相同的工作流。'
},
'download.faq.2.q': {
en: 'How much disk space do I need?',
'zh-CN': '需要多少磁盘空间?'
},
'download.faq.2.a': {
en: 'ComfyUI is lightweight, models are the heavy part. Plan for a dedicated drive as your library grows.',
'zh-CN':
'ComfyUI 本身很轻量,模型才是大头。随着库的增长,建议准备专用硬盘。'
},
'download.faq.3.q': {
en: "Is it really free? What's the catch?",
'zh-CN': '真的免费吗?有什么附加条件?'
},
'download.faq.3.a': {
en: 'Yes. Free and open source under GPL-3.0. No feature gates, no trials, no catch.',
'zh-CN':
'是的。基于 GPL-3.0 免费开源。没有功能限制、没有试用期、没有附加条件。'
},
'download.faq.4.q': {
en: 'Why would I pay for Comfy Cloud if Local is free?',
'zh-CN': '既然本地版免费,为什么还要付费使用 Comfy Cloud'
},
'download.faq.4.a': {
en: 'Your machine or ours. Cloud gives you powerful GPUs on demand, pre-loaded models, end-to-end security and infrastructure out of the box and partner models cleared for commercial use.',
'zh-CN':
'你的机器或我们的。Cloud 按需提供强大 GPU、预加载模型、端到端安全性和开箱即用的基础设施以及经过商业许可的合作伙伴模型。'
},
'download.faq.5.q': {
en: "What's the difference between Desktop, Portable, and CLI install?",
'zh-CN': 'Desktop、Portable 和 CLI 安装有什么区别?'
},
'download.faq.5.a': {
en: 'Desktop: one-click installer with auto-updates. Portable: self-contained build you can run from any folder. CLI: clone from GitHub for full developer control, for developers who want to customize the environment or contribute upstream.',
'zh-CN':
'Desktop一键安装自动更新。Portable独立构建可从任意文件夹运行。CLI从 GitHub 克隆,完全开发者控制,适合想自定义环境或参与上游贡献的开发者。'
},
'download.faq.6.q': {
en: 'Can I use my local workflows in Comfy Cloud?',
'zh-CN': '我可以在 Comfy Cloud 中使用本地工作流吗?'
},
'download.faq.6.a': {
en: 'Yes — same file, same results. No conversion, no rework.',
'zh-CN': '可以——同样的文件,同样的结果。无需转换,无需返工。'
},
'download.faq.7.q': {
en: 'How do I install custom nodes and extensions?',
'zh-CN': '如何安装自定义节点和扩展?'
},
'download.faq.7.a': {
en: 'ComfyUI Manager lets you browse, install, update, and manage 5,000+ extensions from inside the app.',
'zh-CN': 'ComfyUI Manager 让你在应用内浏览、安装、更新和管理 5,000+ 扩展。'
},
'download.faq.8.q': {
en: 'My workflow is running slowly. Should I switch to Cloud?',
'zh-CN': '我的工作流运行缓慢。应该切换到 Cloud 吗?'
},
'download.faq.8.a': {
en: 'No need to switch. Push heavy jobs to Comfy Cloud when you need more compute, keep building locally the rest of the time.',
'zh-CN':
'无需切换。需要更多算力时将繁重任务推送到 Comfy Cloud其余时间继续在本地构建。'
},
// Download EcoSystemSection
'download.ecosystem.heading': {
en: 'An ecosystem that moves faster than any company could.',
'zh-CN': '一个比任何公司都迭代更快的生态系统。'
},
'download.ecosystem.description': {
en: 'Over 5,000 community-built extensions — totaling 60,000+ nodes — plug into ComfyUI and extend what it can do. When a new open model launches, ComfyUI implements it, and the community customizes and builds it into their workflows immediately. When a research paper drops a new technique, an extension appears within days.',
'zh-CN':
'超过 5,000 个社区构建的扩展——共计 60,000+ 节点——接入 ComfyUI 并扩展其能力。当新的开源模型发布时ComfyUI 会实现它,社区会立即将其定制并构建到工作流中。当研究论文发布新技术时,几天内就会出现相应扩展。'
},
// Download ReasonSection
'download.reason.heading': {
en: 'Why\nprofessionals\nchoose ',
'zh-CN': '专业人士为何\n选择'
},
'download.reason.headingHighlight': {
en: 'Local',
'zh-CN': '本地版'
},
'download.reason.1.title': {
en: 'Unlimited\nCustomization',
'zh-CN': '无限\n自定义'
},
'download.reason.1.description': {
en: 'Install any of 5,000+ community extensions, totaling 60,000+ nodes. Build your own custom nodes. Integrate with Photoshop, Nuke, Blender, Houdini, and any tool in your existing pipeline.',
'zh-CN':
'安装 5,000+ 社区扩展中的任何一个,共计 60,000+ 节点。构建自定义节点。与 Photoshop、Nuke、Blender、Houdini 及现有管线中的任何工具集成。'
},
'download.reason.2.title': {
en: 'Any model.\nNo exceptions.',
'zh-CN': '任何模型。\n无一例外。'
},
'download.reason.2.description': {
en: 'Run every open-source model — Wan 2.1, Flux, LTX and more. Finetune, customize, control the full inference process. Or use partner models like Nano Banana and Grok.',
'zh-CN':
'运行每个开源模型——Wan 2.1、Flux、LTX 等。微调、自定义、控制完整推理过程。或使用 Nano Banana 和 Grok 等合作伙伴模型。'
},
'download.reason.3.title': {
en: 'Your machine.\nYour data.\nYour terms.',
'zh-CN': '你的机器。\n你的数据。\n你的规则。'
},
'download.reason.3.description': {
en: 'Run entirely offline. No internet connection required after setup. Your workflows, your models, your data.',
'zh-CN':
'完全离线运行。安装后无需网络连接。你的工作流、你的模型、你的数据。'
},
'download.reason.4.title': {
en: 'Free. Open Source.\nNo ceiling.',
'zh-CN': '免费。开源。\n没有上限。'
},
'download.reason.4.description': {
en: 'No feature gates, no trial periods, no "pro" tier for core functionality. No vendor can lock you in or force you off the platform. Build your own nodes and modify ComfyUI as your own.',
'zh-CN':
'没有功能限制、没有试用期、核心功能没有"专业"层级。没有供应商可以锁定你或强迫你离开平台。构建自己的节点,随心修改 ComfyUI。'
},
// Download HeroSection
'download.hero.heading': {
en: 'Run on your hardware.\nFree forever.',
'zh-CN': '在你的硬件上运行。\n永久免费。'
},
'download.hero.subtitle': {
en: 'The full ComfyUI engine — open source, fast, extensible, and yours to run however you want.',
'zh-CN': '完整的 ComfyUI 引擎——开源、快速、可扩展,随你运行。'
},
'download.hero.downloadLocal': {
en: 'DOWNLOAD LOCAL',
'zh-CN': '下载本地版'
},
'download.hero.installGithub': {
en: 'INSTALL FROM GITHUB',
'zh-CN': '从 GITHUB 安装'
},
// Download CloudBannerSection
'download.cloud.prefix': {
en: 'Need more power?',
'zh-CN': '需要更强算力?'
},
'download.cloud.cta': {
en: 'TRY COMFY CLOUD',
'zh-CN': '试试 COMFY CLOUD'
},
'download.cloud.suffix': {
en: 'Powerful GPUs, same workflow, same results, from anywhere.',
'zh-CN': '强大 GPU同样的工作流同样的结果随时随地。'
},
'buildWhat.row1': { en: 'BUILD WHAT', 'zh-CN': '构建' },
'buildWhat.row2a': { en: "DOESN'T EXIST", 'zh-CN': '尚不存在的' },
'buildWhat.row2b': { en: 'YET', 'zh-CN': '事物' },
@@ -278,4 +774,4 @@ export function t(key: TranslationKey, locale: Locale = 'en'): string {
return translations[key][locale] ?? translations[key].en
}
export type { Locale }
export type { Locale, TranslationKey }

View File

@@ -1,8 +1,18 @@
---
import BaseLayout from '../layouts/BaseLayout.astro'
import ComingSoon from '../components/common/ComingSoon.astro'
import CloudBannerSection from '../components/product/shared/CloudBannerSection.vue'
import HeroSection from '../components/product/api/HeroSection.vue'
import AutomationSection from '../components/product/api/AutomationSection.vue'
import StepsSection from '../components/product/api/StepsSection.vue'
import ReasonSection from '../components/product/api/ReasonSection.vue'
import ProductCardsSection from '../components/common/ProductCardsSection.vue'
---
<BaseLayout title="Comfy API">
<ComingSoon />
<CloudBannerSection />
<HeroSection />
<AutomationSection />
<StepsSection />
<ReasonSection />
<ProductCardsSection exclude-product="api" label-key="products.labelProducts" />
</BaseLayout>

View File

@@ -1,8 +1,24 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro'
import ComingSoon from '../../components/common/ComingSoon.astro'
import CloudBannerSection from '../../components/product/shared/CloudBannerSection.vue'
import HeroSection from '../../components/product/enterprise/HeroSection.vue'
import SocialProofBarSection from '../../components/common/SocialProofBarSection.vue'
import ReasonSection from '../../components/product/enterprise/ReasonSection.vue'
import TeamSection from '../../components/product/enterprise/TeamSection.vue'
import DataOwnershipSection from '../../components/product/enterprise/DataOwnershipSection.vue'
import BYOKeySection from '../../components/product/enterprise/BYOKeySection.vue'
import OrchestrationSection from '../../components/product/enterprise/OrchestrationSection.vue'
import ProductCardsSection from '../../components/common/ProductCardsSection.vue'
---
<BaseLayout title="Comfy Enterprise">
<ComingSoon />
<CloudBannerSection />
<HeroSection />
<SocialProofBarSection />
<ReasonSection />
<TeamSection />
<DataOwnershipSection />
<BYOKeySection />
<OrchestrationSection />
<ProductCardsSection exclude-product="enterprise" label-key="products.labelProducts" />
</BaseLayout>

View File

@@ -1,8 +1,18 @@
---
import BaseLayout from '../layouts/BaseLayout.astro'
import ComingSoon from '../components/common/ComingSoon.astro'
import HeroSection from '../components/product/local/HeroSection.vue'
import CloudBannerSection from '../components/product/shared/CloudBannerSection.vue'
import ReasonSection from '../components/product/local/ReasonSection.vue'
import EcoSystemSection from '../components/product/local/EcoSystemSection.vue'
import ProductCardsSection from '../components/product/local/ProductCardsSection.vue'
import FAQSection from '../components/product/local/FAQSection.vue'
---
<BaseLayout title="Download Comfy — Run AI Locally">
<ComingSoon />
<CloudBannerSection />
<HeroSection client:load />
<ReasonSection />
<EcoSystemSection client:visible />
<ProductCardsSection />
<FAQSection client:visible />
</BaseLayout>

View File

@@ -2,7 +2,7 @@
import BaseLayout from '../layouts/BaseLayout.astro'
import ProductCardsSection from '../components/home/ProductCardsSection.vue'
import HeroSection from '../components/home/HeroSection.vue'
import SocialProofBarSection from '../components/home/SocialProofBarSection.vue'
import SocialProofBarSection from '../components/common/SocialProofBarSection.vue'
import ProductShowcaseSection from '../components/home/ProductShowcaseSection.vue'
import UseCaseSection from '../components/home/UseCaseSection.vue'
import CaseStudySpotlightSection from '../components/home/CaseStudySpotlightSection.vue'
@@ -13,8 +13,8 @@ import BuildWhatSection from '../components/home/BuildWhatSection.vue'
<BaseLayout title="Comfy — Professional Control of Visual AI">
<HeroSection client:load />
<SocialProofBarSection />
<ProductShowcaseSection client:visible />
<UseCaseSection client:visible />
<ProductShowcaseSection client:load />
<UseCaseSection client:load />
<GetStartedSection />
<ProductCardsSection />
<CaseStudySpotlightSection />

View File

@@ -0,0 +1,18 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro'
import CloudBannerSection from '../../components/product/shared/CloudBannerSection.vue'
import HeroSection from '../../components/product/api/HeroSection.vue'
import AutomationSection from '../../components/product/api/AutomationSection.vue'
import StepsSection from '../../components/product/api/StepsSection.vue'
import ReasonSection from '../../components/product/api/ReasonSection.vue'
import ProductCardsSection from '../../components/common/ProductCardsSection.vue'
---
<BaseLayout title="Comfy API">
<CloudBannerSection locale="zh-CN" />
<HeroSection locale="zh-CN" />
<AutomationSection locale="zh-CN" />
<StepsSection locale="zh-CN" />
<ReasonSection locale="zh-CN" />
<ProductCardsSection locale="zh-CN" exclude-product="api" label-key="products.labelProducts" />
</BaseLayout>

View File

@@ -0,0 +1,16 @@
---
import BaseLayout from '../../../layouts/BaseLayout.astro'
import CloudBannerSection from '../../../components/product/shared/CloudBannerSection.vue'
import HeroSection from '../../../components/product/enterprise/HeroSection.vue'
import SocialProofBarSection from '../../../components/common/SocialProofBarSection.vue'
import ReasonSection from '../../../components/product/enterprise/ReasonSection.vue'
import TeamSection from '../../../components/product/enterprise/TeamSection.vue'
---
<BaseLayout title="Comfy Enterprise">
<CloudBannerSection locale="zh-CN" />
<HeroSection locale="zh-CN" />
<SocialProofBarSection />
<ReasonSection locale="zh-CN" />
<TeamSection locale="zh-CN" />
</BaseLayout>

View File

@@ -1,76 +1,18 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro'
const cards = [
{
icon: '🪟',
title: 'Windows',
description: '需要 NVIDIA 或 AMD 显卡',
cta: '下载 Windows 版',
href: 'https://download.comfy.org/windows/nsis/x64',
outlined: false,
},
{
icon: '🍎',
title: 'Mac',
description: '需要 Apple Silicon (M 系列)',
cta: '下载 Mac 版',
href: 'https://download.comfy.org/mac/dmg/arm64',
outlined: false,
},
{
icon: '🐙',
title: 'GitHub',
description: '在任何平台上从源码构建',
cta: '从 GitHub 安装',
href: 'https://github.com/comfyanonymous/ComfyUI',
outlined: true,
},
]
import CloudBannerSection from '../../components/product/shared/CloudBannerSection.vue'
import HeroSection from '../../components/product/local/HeroSection.vue'
import ReasonSection from '../../components/product/local/ReasonSection.vue'
import EcoSystemSection from '../../components/product/local/EcoSystemSection.vue'
import ProductCardsSection from '../../components/product/local/ProductCardsSection.vue'
import FAQSection from '../../components/product/local/FAQSection.vue'
---
<BaseLayout title="下载 — Comfy">
<div class="mx-auto max-w-5xl px-6 py-32 text-center">
<h1 class="text-4xl font-bold text-white md:text-5xl">
下载 ComfyUI
</h1>
<p class="mt-4 text-lg text-smoke-700">
在本地体验 AI 创作
</p>
<div class="mt-16 grid grid-cols-1 gap-6 md:grid-cols-3">
{cards.map((card) => (
<a
href={card.href}
class="flex flex-col items-center rounded-xl border border-white/10 bg-charcoal-600 p-8 text-center transition-colors hover:border-brand-yellow"
>
<span class="text-4xl" aria-hidden="true">{card.icon}</span>
<h2 class="mt-4 text-xl font-semibold text-white">{card.title}</h2>
<p class="mt-2 text-sm text-smoke-700">{card.description}</p>
<span
class:list={[
'mt-6 inline-block rounded-full px-6 py-2 text-sm font-semibold transition-opacity hover:opacity-90',
card.outlined
? 'border border-brand-yellow text-brand-yellow'
: 'bg-brand-yellow text-black',
]}
>
{card.cta}
</span>
</a>
))}
</div>
<div class="mt-20 rounded-xl border border-white/10 bg-charcoal-800 p-8">
<p class="text-lg text-smoke-700">
没有 GPU{' '}
<a
href="https://app.comfy.org"
class="font-semibold text-brand-yellow hover:underline"
>
试试 Comfy Cloud →
</a>
</p>
</div>
</div>
<CloudBannerSection locale="zh-CN" />
<HeroSection locale="zh-CN" client:load />
<ReasonSection locale="zh-CN" />
<EcoSystemSection locale="zh-CN" client:visible />
<ProductCardsSection locale="zh-CN" />
<FAQSection locale="zh-CN" client:visible />
</BaseLayout>

View File

@@ -2,7 +2,7 @@
import BaseLayout from '../../layouts/BaseLayout.astro'
import ProductCardsSection from '../../components/home/ProductCardsSection.vue'
import HeroSection from '../../components/home/HeroSection.vue'
import SocialProofBarSection from '../../components/home/SocialProofBarSection.vue'
import SocialProofBarSection from '../../components/common/SocialProofBarSection.vue'
import ProductShowcaseSection from '../../components/home/ProductShowcaseSection.vue'
import UseCaseSection from '../../components/home/UseCaseSection.vue'
import CaseStudySpotlightSection from '../../components/home/CaseStudySpotlightSection.vue'

View File

@@ -113,3 +113,16 @@
scroll-behavior: auto !important;
}
}
@utility icon-mask {
background-color: currentColor;
mask-size: contain;
mask-repeat: no-repeat;
mask-position: center;
}
:root {
--site-bg: #211927;
--site-bg-soft: color-mix(in srgb, var(--site-bg) 88%, black 12%);
--site-border-subtle: rgb(255 255 255 / 0.1);
}

View File

@@ -6,5 +6,6 @@
}
},
"include": ["src/**/*", "e2e/**/*", "astro.config.ts"],
"exclude": ["src/**/*.stories.ts"]
"exclude": ["src/**/*.stories.ts"],
"references": [{ "path": "./tsconfig.stories.json" }]
}

View File

@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2023",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"skipLibCheck": true,
"composite": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*.stories.ts"]
}