mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-05 21:54:50 +00:00
Compare commits
11 Commits
v1.44.17
...
feat/websi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ffe1b4ce9d | ||
|
|
0fa90b0177 | ||
|
|
2b4afd132f | ||
|
|
0b35b7acfc | ||
|
|
42cc892869 | ||
|
|
b25d8c23c8 | ||
|
|
e10dfb98eb | ||
|
|
97c50a30a7 | ||
|
|
10f0602b20 | ||
|
|
c7833ca5f1 | ||
|
|
47293e1203 |
167
apps/website/e2e/download.spec.ts
Normal file
167
apps/website/e2e/download.spec.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
3
apps/website/public/icons/node-union-2size.svg
Normal file
3
apps/website/public/icons/node-union-2size.svg
Normal 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 |
32
apps/website/src/components/common/FAQSection.stories.ts
Normal file
32
apps/website/src/components/common/FAQSection.stories.ts
Normal 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
|
||||
}
|
||||
}
|
||||
98
apps/website/src/components/common/FAQSection.vue
Normal file
98
apps/website/src/components/common/FAQSection.vue
Normal 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>
|
||||
74
apps/website/src/components/common/ProductCardsSection.vue
Normal file
74
apps/website/src/components/common/ProductCardsSection.vue
Normal 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>
|
||||
56
apps/website/src/components/common/ProductHeroBadge.vue
Normal file
56
apps/website/src/components/common/ProductHeroBadge.vue
Normal 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>
|
||||
@@ -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
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
124
apps/website/src/components/product/api/HeroSection.vue
Normal file
124
apps/website/src/components/product/api/HeroSection.vue
Normal 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>
|
||||
37
apps/website/src/components/product/api/ReasonSection.vue
Normal file
37
apps/website/src/components/product/api/ReasonSection.vue
Normal 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>
|
||||
93
apps/website/src/components/product/api/StepsSection.vue
Normal file
93
apps/website/src/components/product/api/StepsSection.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
122
apps/website/src/components/product/enterprise/HeroSection.vue
Normal file
122
apps/website/src/components/product/enterprise/HeroSection.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
16
apps/website/src/components/product/local/FAQSection.vue
Normal file
16
apps/website/src/components/product/local/FAQSection.vue
Normal 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>
|
||||
85
apps/website/src/components/product/local/HeroSection.vue
Normal file
85
apps/website/src/components/product/local/HeroSection.vue
Normal 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>
|
||||
@@ -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>
|
||||
35
apps/website/src/components/product/local/ReasonSection.vue
Normal file
35
apps/website/src/components/product/local/ReasonSection.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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.'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
62
apps/website/src/components/product/shared/ReasonSection.vue
Normal file
62
apps/website/src/components/product/shared/ReasonSection.vue
Normal 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>
|
||||
28
apps/website/src/composables/useDownloadUrl.ts
Normal file
28
apps/website/src/composables/useDownloadUrl.ts
Normal 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() }
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
18
apps/website/src/pages/zh-CN/api.astro
Normal file
18
apps/website/src/pages/zh-CN/api.astro
Normal 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>
|
||||
16
apps/website/src/pages/zh-CN/cloud/enterprise.astro
Normal file
16
apps/website/src/pages/zh-CN/cloud/enterprise.astro
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -6,5 +6,6 @@
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*", "e2e/**/*", "astro.config.ts"],
|
||||
"exclude": ["src/**/*.stories.ts"]
|
||||
"exclude": ["src/**/*.stories.ts"],
|
||||
"references": [{ "path": "./tsconfig.stories.json" }]
|
||||
}
|
||||
|
||||
16
apps/website/tsconfig.stories.json
Normal file
16
apps/website/tsconfig.stories.json
Normal 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"]
|
||||
}
|
||||
Reference in New Issue
Block a user