Compare commits
6 Commits
fix/remove
...
feat/websi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a1b6cddfc | ||
|
|
49f5312618 | ||
|
|
4afa9c9537 | ||
|
|
e941556d4a | ||
|
|
639b598613 | ||
|
|
e019a0754a |
137
apps/website/e2e/cloud.spec.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
test.describe('Cloud page @smoke', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/cloud')
|
||||
})
|
||||
|
||||
test('has correct title', async ({ page }) => {
|
||||
await expect(page).toHaveTitle('Comfy Cloud — AI in the Cloud')
|
||||
})
|
||||
|
||||
test('HeroSection heading and subtitle are visible', async ({ page }) => {
|
||||
await expect(
|
||||
page.getByRole('heading', { name: /The full power of/i, level: 1 })
|
||||
).toBeVisible()
|
||||
await expect(
|
||||
page.getByText(/The easiest way to start with ComfyUI/)
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('HeroSection has CTA button linking to cloud', async ({ page }) => {
|
||||
const cta = page.getByRole('link', { name: /TRY COMFY CLOUD FOR FREE/i })
|
||||
await expect(cta).toBeVisible()
|
||||
})
|
||||
|
||||
test('ReasonSection heading and reasons are visible', async ({ page }) => {
|
||||
await expect(
|
||||
page.getByRole('heading', { name: /Why.*professionals.*choose/i })
|
||||
).toBeVisible()
|
||||
|
||||
for (const title of [
|
||||
'Powerful GPUs',
|
||||
'All models',
|
||||
'More control',
|
||||
'Community workflows'
|
||||
]) {
|
||||
await expect(page.getByText(title).first()).toBeVisible()
|
||||
}
|
||||
})
|
||||
|
||||
test('AIModelsSection heading and 5 model cards are visible', async ({
|
||||
page
|
||||
}) => {
|
||||
await expect(
|
||||
page.getByRole('heading', { name: /leading AI models/i })
|
||||
).toBeVisible()
|
||||
|
||||
const grid = page.locator('.grid', {
|
||||
has: page.getByText('Grok Imagine')
|
||||
})
|
||||
const modelCards = grid.locator('a[href="https://comfy.org/workflows"]')
|
||||
await expect(modelCards).toHaveCount(5)
|
||||
})
|
||||
|
||||
test('AIModelsSection CTA links to workflows', async ({ page }) => {
|
||||
const cta = page.getByRole('link', {
|
||||
name: /EXPLORE WORKFLOWS/i
|
||||
})
|
||||
await expect(cta.first()).toBeVisible()
|
||||
await expect(cta.first()).toHaveAttribute(
|
||||
'href',
|
||||
'https://comfy.org/workflows'
|
||||
)
|
||||
})
|
||||
|
||||
test('AudienceSection heading and cards are visible', async ({ page }) => {
|
||||
await expect(page.getByText(/creators/i).first()).toBeVisible()
|
||||
|
||||
for (const label of ['CREATORS', 'TEAMS & STUDIOS']) {
|
||||
await expect(page.getByText(label).first()).toBeVisible()
|
||||
}
|
||||
})
|
||||
|
||||
test('PricingSection heading and CTA are visible', async ({ page }) => {
|
||||
await expect(page.getByText(/Simple, credit-based pricing/)).toBeVisible()
|
||||
|
||||
const cta = page.getByRole('link', { name: /SEE PRICING PLANS/i })
|
||||
await expect(cta).toBeVisible()
|
||||
await expect(cta).toHaveAttribute('href', '/cloud/pricing')
|
||||
})
|
||||
|
||||
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('FAQSection heading is visible with 15 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(15)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Cloud FAQ accordion @interaction', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/cloud')
|
||||
})
|
||||
|
||||
test('all FAQs are expanded by default', async ({ page }) => {
|
||||
await expect(
|
||||
page.getByText(/Comfy Cloud is a version of ComfyUI/i)
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('clicking an expanded FAQ collapses it', async ({ page }) => {
|
||||
const firstQuestion = page.getByRole('button', {
|
||||
name: /What is Comfy Cloud/i
|
||||
})
|
||||
await firstQuestion.scrollIntoViewIfNeeded()
|
||||
await firstQuestion.click()
|
||||
|
||||
await expect(
|
||||
page.getByText(/Comfy Cloud is a version of ComfyUI/i)
|
||||
).toBeHidden()
|
||||
})
|
||||
|
||||
test('clicking a collapsed FAQ expands it again', async ({ page }) => {
|
||||
const firstQuestion = page.getByRole('button', {
|
||||
name: /What is Comfy Cloud/i
|
||||
})
|
||||
await firstQuestion.scrollIntoViewIfNeeded()
|
||||
|
||||
await firstQuestion.click()
|
||||
await expect(
|
||||
page.getByText(/Comfy Cloud is a version of ComfyUI/i)
|
||||
).toBeHidden()
|
||||
|
||||
await firstQuestion.click()
|
||||
await expect(
|
||||
page.getByText(/Comfy Cloud is a version of ComfyUI/i)
|
||||
).toBeVisible()
|
||||
})
|
||||
})
|
||||
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/ai-models/gemini.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M31.0589 13.0316H16.014V19.1831H24.6589C24.5199 20.0537 24.2078 20.9101 23.7509 21.691C23.2273 22.5858 22.5801 23.2669 21.9167 23.7857C19.9294 25.3395 17.6126 25.6572 16.0035 25.6572C11.9389 25.6572 8.46594 22.9766 7.12148 19.3341C7.06723 19.2019 7.0312 19.0654 6.98733 18.9304C6.69023 18.0034 6.5279 17.0215 6.5279 16.001C6.5279 14.939 6.70369 13.9223 7.0242 12.9621C8.28844 9.17522 11.8397 6.34675 16.0064 6.34675C16.8445 6.34675 17.6516 6.44854 18.417 6.65159C20.1661 7.11561 21.4034 8.0295 22.1615 8.75237L26.7361 4.18101C23.9534 1.57756 20.3259 3.9369e-09 15.9988 3.9369e-09C12.5396 -7.59723e-05 9.34593 1.09971 6.72881 2.95838C4.60641 4.46571 2.86573 6.48384 1.69099 8.82767C0.598311 11.0009 0 13.4092 0 15.9986C0 18.5881 0.599225 21.0215 1.69191 23.1746V23.1891C2.84605 25.4749 4.5338 27.4431 6.58508 28.9435C8.3771 30.2543 11.5904 32 15.9988 32C18.534 32 20.7809 31.5336 22.7625 30.6595C24.192 30.029 25.4585 29.2066 26.6052 28.1496C28.1203 26.753 29.3069 25.0255 30.1168 23.038C30.9268 21.0505 31.36 18.8029 31.36 16.3662C31.36 15.2314 31.2483 14.0791 31.0589 13.0316Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
4
apps/website/public/icons/ai-models/grok.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="32" height="31" viewBox="0 0 32 31" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.3587 19.8988L22.9972 11.9646C23.5187 11.5757 24.2642 11.7274 24.5127 12.3316C25.8207 15.5179 25.2363 19.3471 22.634 21.9762C20.0318 24.6052 16.411 25.1818 13.1015 23.8686L9.48617 25.5598C14.6716 29.1406 20.9684 28.255 24.9032 24.277C28.0243 21.1237 28.9909 16.8255 28.0871 12.9496L28.0952 12.9578C26.7845 7.26377 28.4175 4.98781 31.7625 0.333808C31.8416 0.223459 31.9208 0.113108 32 0L27.5982 4.44709V4.4333L12.356 19.9016" fill="white"/>
|
||||
<path d="M10.1634 21.8272C6.44151 18.2353 7.0832 12.6764 10.2589 9.47079C12.6072 7.09824 16.4546 6.12993 19.8133 7.55344L23.4204 5.87061C22.7706 5.3961 21.9377 4.88574 20.982 4.52709C16.6622 2.73116 11.4904 3.62499 7.97884 7.16997C4.60108 10.5825 3.53887 15.8297 5.36292 20.3071C6.72549 23.6535 4.49185 26.0204 2.24183 28.4096C1.44449 29.2564 0.644421 30.1034 0 31L10.1606 21.8299" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 952 B |
5
apps/website/public/icons/ai-models/ltx.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="38" height="16" viewBox="0 0 38 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M25.6714 2.88536C26.1493 2.82951 26.7876 2.87631 27.2746 2.87552C29.4061 2.87217 29.7407 4.85107 31.0086 6.17285C31.3057 5.47951 31.7391 4.76441 32.1866 4.149C32.6673 3.48769 33.0911 3.10066 33.953 2.95519C34.7328 2.82353 36.8597 2.74599 37.4893 3.17876C37.6223 3.27022 37.7198 3.40009 37.7342 3.55864C37.7879 4.1504 34.524 8.38331 33.8977 9.14783C34.6224 10.5615 37.9453 14.0003 37.9993 15.2938C38.0058 15.4508 37.9689 15.5748 37.8512 15.6902C37.4841 16.0502 36.5552 15.9679 36.0583 15.9783C35.4029 15.9928 34.8918 16.0117 34.2446 15.9184C32.7906 15.7089 32.4029 14.8649 31.7106 13.8079C31.4542 13.4166 31.1547 13.018 30.868 12.641C29.8939 13.549 29.4433 14.7027 28.431 15.5215C27.7186 16.0978 24.0748 16.1659 23.6489 15.5402C23.3554 14.4588 26.2687 11.0941 26.96 10.1276C27.1912 9.80439 27.3923 9.66031 27.5531 9.23699L27.5618 9.21374C27.0682 8.21459 23.6269 4.06408 23.8836 3.31986C24.2686 2.90918 25.1029 2.92321 25.6714 2.88536Z" fill="white"/>
|
||||
<path d="M13.5798 0.0217006C14.0245 0.0102399 17.731 -0.0733401 17.8671 0.188355C18.0783 0.594232 18.0745 2.39664 17.9571 2.91885C18.7829 2.90052 20.9815 2.73358 21.576 3.24255C21.9254 3.54165 21.8894 6.31329 21.4914 6.7223C20.9809 6.93681 18.6254 6.86654 17.9992 6.8141C18.2005 8.24188 17.6814 9.92179 18.1662 11.2688C18.2188 11.4149 18.4418 11.6515 18.5582 11.7758L18.5851 11.7807C19.4248 11.9253 20.4783 11.7921 21.4193 11.8541C21.5894 11.9423 21.7529 12.0279 21.8509 12.193C22.2597 12.8822 22.1381 14.3802 21.9594 15.1114C21.9191 15.307 21.8419 15.6435 21.6653 15.748C20.9367 16.1792 19.0596 15.9477 18.2447 15.8484C16.5346 15.6399 15.0424 15.373 13.9488 14.018C12.762 12.5472 12.9954 10.9199 12.9709 9.17573C12.9618 8.52672 12.9588 7.41995 13.0502 6.80867C12.4361 6.84166 10.8569 7.01497 10.4917 6.5846C10.1043 6.12824 10.3072 3.9303 10.3518 3.3296C10.9517 2.70004 12.1816 2.90197 13.0395 2.9117C13.0534 2.19219 12.7947 0.87298 13.1333 0.195009C13.203 0.055524 13.4145 0.0516102 13.5798 0.0217006Z" fill="white"/>
|
||||
<path d="M1.66628 2.88823C2.58555 2.81214 4.10841 2.81874 4.945 3.20141C5.33248 5.15667 4.99029 7.60389 5.06565 9.60325C5.07579 9.8735 4.9874 11.6351 5.25399 11.7357C6.28618 12.1249 11.1291 11.506 11.8399 12.0455C12.163 12.56 12.0235 15.1134 11.8775 15.7598C11.3062 15.9103 11.0189 15.8876 10.4264 15.8899L5.78094 15.8919C3.59892 15.9082 2.36972 16.0953 0.208639 15.5943C0.116058 15.3247 0.0349184 14.7783 0.0303189 14.4959C-0.0272935 10.99 0.0150458 7.4781 0.0142792 3.97146C0.0140433 2.80644 0.604555 2.93016 1.66628 2.88823Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
4
apps/website/public/icons/ai-models/qwen.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.1935 32L24.9858 15.5745L27.22 19.5198H31.4952L27.22 12.0998L29.4765 8.3063H12.2615L14.5178 4.36093L12.2615 0.332235L7.41455 8.3063H2.39172L11.6896 23.9422H7.41455L4.85911 27.9711H13.194L15.6878 32L15.6431 31.9985C15.4212 31.9838 15.2231 31.8597 15.1159 31.668L13.1938 28.3031H4.85911C4.6194 28.3031 4.40156 28.1756 4.2872 27.9711L2.03092 24.1776C1.85061 23.8609 1.8506 23.4694 2.03092 23.1527L3.95303 19.7881L0.135229 12.9026C-0.0450889 12.586 -0.0450637 12.1944 0.135229 11.8778L2.39151 8.08424C2.50586 7.87972 2.72371 7.75222 2.96341 7.75222H7.41455L11.6896 0.332235C11.8039 0.127714 12.0218 0 12.2615 0H16.7124C16.9521 0 17.17 0.127714 17.2843 0.332235L19.2064 3.69689H27.5411C27.7808 3.69689 27.9986 3.82439 28.113 4.02891L30.3693 7.82245C30.5496 8.13912 30.5496 8.53061 30.3693 8.84729L28.113 12.6408L31.6316 19.0976C31.8119 19.4143 31.8119 19.8058 31.6316 20.1224L29.3751 23.916C29.2608 24.1204 29.0431 24.248 28.8034 24.248H24.9856L20.7106 31.668C20.5962 31.8725 20.3784 32 20.1387 32H16.1935Z" fill="white"/>
|
||||
<path d="M22.0609 13.703H10.6869L16.1935 23.001L22.0609 13.703Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
4
apps/website/public/icons/ai-models/wan.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.1935 32L24.9858 15.5745L27.22 19.5198H31.4952L27.22 12.0998L29.4765 8.3063H12.2615L14.5178 4.36093L12.2615 0.332235L7.41455 8.3063H2.39172L11.6896 23.9422H7.41455L4.85911 27.9711H13.194L15.6878 32L15.6431 31.9985C15.4212 31.9838 15.2231 31.8597 15.1159 31.668L13.1938 28.3031H4.85911C4.6194 28.3031 4.40156 28.1756 4.2872 27.9711L2.03092 24.1776C1.85061 23.8609 1.8506 23.4694 2.03092 23.1527L3.95303 19.7881L0.135229 12.9026C-0.0450889 12.586 -0.0450637 12.1944 0.135229 11.8778L2.39151 8.08424C2.50586 7.87972 2.72371 7.75222 2.96341 7.75222H7.41455L11.6896 0.332235C11.8039 0.127714 12.0218 0 12.2615 0H16.7124C16.9521 0 17.17 0.127714 17.2843 0.332235L19.2064 3.69689H27.5411C27.7808 3.69689 27.9986 3.82439 28.113 4.02891L30.3693 7.82245C30.5496 8.13912 30.5496 8.53061 30.3693 8.84729L28.113 12.6408L31.6316 19.0976C31.8119 19.4143 31.8119 19.8058 31.6316 20.1224L29.3751 23.916C29.2608 24.1204 29.0431 24.248 28.8034 24.248H24.9856L20.7106 31.668C20.5962 31.8725 20.3784 32 20.1387 32H16.1935Z" fill="white"/>
|
||||
<path d="M22.0609 13.703H10.6869L16.1935 23.001L22.0609 13.703Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
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 |
BIN
apps/website/public/images/cloud/ai-models/grok-imagine.webp
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
apps/website/public/images/cloud/ai-models/ltx-23.webp
Normal file
|
After Width: | Height: | Size: 640 KiB |
BIN
apps/website/public/images/cloud/ai-models/nano-banana-pro.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
apps/website/public/images/cloud/ai-models/qwen-image-edit.webp
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
apps/website/public/images/cloud/ai-models/wan-22.webp
Normal file
|
After Width: | Height: | Size: 978 KiB |
BIN
apps/website/public/images/cloud/audience-creator.jpg
Normal file
|
After Width: | Height: | Size: 181 KiB |
BIN
apps/website/public/images/cloud/audience-team.jpg
Normal file
|
After Width: | Height: | Size: 71 KiB |
110
apps/website/public/images/cloud/hero-cube.svg
Normal file
@@ -0,0 +1,110 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 900" fill="none">
|
||||
<!-- Background geometric lines -->
|
||||
<g stroke="#49378B" stroke-width="1.5" fill="none" opacity="0.4">
|
||||
<!-- Outer hexagonal frame layers -->
|
||||
<path d="M400 80 L600 200 L600 440 L400 560 L200 440 L200 200 Z" />
|
||||
<path d="M400 120 L570 220 L570 420 L400 520 L230 420 L230 220 Z" />
|
||||
<!-- Connector lines going up -->
|
||||
<line x1="300" y1="160" x2="300" y2="60" />
|
||||
<line x1="400" y1="120" x2="400" y2="20" />
|
||||
<line x1="500" y1="160" x2="500" y2="60" />
|
||||
<!-- Bottom platform layers -->
|
||||
<path d="M250 520 L550 520 L600 560 L600 600 L400 700 L200 600 L200 560 Z" opacity="0.3" />
|
||||
<path d="M280 620 L520 620 L560 650 L560 680 L400 760 L240 680 L240 650 Z" opacity="0.2" />
|
||||
<path d="M320 700 L480 700 L510 720 L510 740 L400 800 L290 740 L290 720 Z" opacity="0.15" />
|
||||
</g>
|
||||
|
||||
<!-- 3D Isometric cube cluster -->
|
||||
<g transform="translate(400, 380)">
|
||||
<!-- Back layer cubes (purple/dark) -->
|
||||
<!-- Top back -->
|
||||
<g transform="translate(0, -100)">
|
||||
<polygon points="0,-40 35,-20 35,20 0,40 -35,20 -35,-20" fill="#49378B" />
|
||||
<polygon points="0,-40 35,-20 0,-5 -35,-20" fill="#5a45a0" />
|
||||
<polygon points="0,-5 35,-20 35,20 0,40" fill="#3d2d75" />
|
||||
</g>
|
||||
|
||||
<!-- Middle row - left back -->
|
||||
<g transform="translate(-70, -55)">
|
||||
<polygon points="0,-40 35,-20 35,20 0,40 -35,20 -35,-20" fill="#49378B" />
|
||||
<polygon points="0,-40 35,-20 0,-5 -35,-20" fill="#5a45a0" />
|
||||
<polygon points="0,-5 35,-20 35,20 0,40" fill="#3d2d75" />
|
||||
</g>
|
||||
<!-- Middle row - right back -->
|
||||
<g transform="translate(70, -55)">
|
||||
<polygon points="0,-40 35,-20 35,20 0,40 -35,20 -35,-20" fill="#49378B" />
|
||||
<polygon points="0,-40 35,-20 0,-5 -35,-20" fill="#5a45a0" />
|
||||
<polygon points="0,-5 35,-20 35,20 0,40" fill="#3d2d75" />
|
||||
</g>
|
||||
|
||||
<!-- Yellow accent cubes - front facing -->
|
||||
<!-- Top -->
|
||||
<g transform="translate(0, -65)">
|
||||
<polygon points="0,-40 35,-20 35,20 0,40 -35,20 -35,-20" fill="#f2ff59" />
|
||||
<polygon points="0,-40 35,-20 0,-5 -35,-20" fill="#f2ff59" />
|
||||
<polygon points="0,-5 35,-20 35,20 0,40" fill="#d4e04e" />
|
||||
<polygon points="0,-5 -35,-20 -35,20 0,40" fill="#e0ec50" />
|
||||
</g>
|
||||
|
||||
<!-- Middle left yellow -->
|
||||
<g transform="translate(-70, -20)">
|
||||
<polygon points="0,-40 35,-20 35,20 0,40 -35,20 -35,-20" fill="#f2ff59" />
|
||||
<polygon points="0,-40 35,-20 0,-5 -35,-20" fill="#f2ff59" />
|
||||
<polygon points="0,-5 35,-20 35,20 0,40" fill="#d4e04e" />
|
||||
<polygon points="0,-5 -35,-20 -35,20 0,40" fill="#e0ec50" />
|
||||
</g>
|
||||
<!-- Middle right yellow -->
|
||||
<g transform="translate(70, -20)">
|
||||
<polygon points="0,-40 35,-20 35,20 0,40 -35,20 -35,-20" fill="#f2ff59" />
|
||||
<polygon points="0,-40 35,-20 0,-5 -35,-20" fill="#f2ff59" />
|
||||
<polygon points="0,-5 35,-20 35,20 0,40" fill="#d4e04e" />
|
||||
<polygon points="0,-5 -35,-20 -35,20 0,40" fill="#e0ec50" />
|
||||
</g>
|
||||
|
||||
<!-- Center purple -->
|
||||
<g transform="translate(0, -20)">
|
||||
<polygon points="0,-40 35,-20 35,20 0,40 -35,20 -35,-20" fill="#49378B" />
|
||||
<polygon points="0,-40 35,-20 0,-5 -35,-20" fill="#5a45a0" />
|
||||
<polygon points="0,-5 35,-20 35,20 0,40" fill="#3d2d75" />
|
||||
</g>
|
||||
|
||||
<!-- Bottom row -->
|
||||
<g transform="translate(-70, 25)">
|
||||
<polygon points="0,-40 35,-20 35,20 0,40 -35,20 -35,-20" fill="#49378B" />
|
||||
<polygon points="0,-40 35,-20 0,-5 -35,-20" fill="#5a45a0" />
|
||||
<polygon points="0,-5 35,-20 35,20 0,40" fill="#3d2d75" />
|
||||
</g>
|
||||
<g transform="translate(70, 25)">
|
||||
<polygon points="0,-40 35,-20 35,20 0,40 -35,20 -35,-20" fill="#49378B" />
|
||||
<polygon points="0,-40 35,-20 0,-5 -35,-20" fill="#5a45a0" />
|
||||
<polygon points="0,-5 35,-20 35,20 0,40" fill="#3d2d75" />
|
||||
</g>
|
||||
|
||||
<!-- Front bottom yellow -->
|
||||
<g transform="translate(0, 25)">
|
||||
<polygon points="0,-40 35,-20 35,20 0,40 -35,20 -35,-20" fill="#f2ff59" />
|
||||
<polygon points="0,-40 35,-20 0,-5 -35,-20" fill="#f2ff59" />
|
||||
<polygon points="0,-5 35,-20 35,20 0,40" fill="#d4e04e" />
|
||||
<polygon points="0,-5 -35,-20 -35,20 0,40" fill="#e0ec50" />
|
||||
</g>
|
||||
|
||||
<!-- Outer corner yellow accents -->
|
||||
<g transform="translate(-105, 5)">
|
||||
<polygon points="0,-25 20,-12 20,12 0,25 -20,12 -20,-12" fill="#f2ff59" />
|
||||
<polygon points="0,-25 20,-12 0,-2 -20,-12" fill="#f2ff59" />
|
||||
<polygon points="0,-2 20,-12 20,12 0,25" fill="#d4e04e" />
|
||||
</g>
|
||||
<g transform="translate(105, 5)">
|
||||
<polygon points="0,-25 20,-12 20,12 0,25 -20,12 -20,-12" fill="#f2ff59" />
|
||||
<polygon points="0,-25 20,-12 0,-2 -20,-12" fill="#f2ff59" />
|
||||
<polygon points="0,-2 -20,-12 -20,12 0,25" fill="#e0ec50" />
|
||||
</g>
|
||||
<g transform="translate(0, -135)">
|
||||
<polygon points="0,-25 20,-12 20,12 0,25 -20,12 -20,-12" fill="#f2ff59" />
|
||||
<polygon points="0,-25 20,-12 0,-2 -20,-12" fill="#f2ff59" />
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- Bottom arrow/chevron shape -->
|
||||
<path d="M340 780 L400 820 L460 780 L460 850 L400 890 L340 850 Z" fill="#211927" stroke="#49378B" stroke-width="1" opacity="0.5" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.0 KiB |
@@ -8,7 +8,7 @@ const {
|
||||
className = ''
|
||||
} = defineProps<{
|
||||
href: string
|
||||
label: string
|
||||
label?: string
|
||||
variant?: 'solid' | 'outline'
|
||||
className?: string
|
||||
}>()
|
||||
@@ -27,6 +27,6 @@ const {
|
||||
)
|
||||
"
|
||||
>
|
||||
{{ label }}
|
||||
<slot>{{ label }}</slot>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
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
@@ -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
@@ -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
@@ -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>
|
||||
@@ -24,7 +24,7 @@ useFrameScrub(canvasRef, {
|
||||
|
||||
<template>
|
||||
<section
|
||||
class="flex flex-col items-center px-4 pt-36 pb-16 lg:px-20 lg:pt-50 lg:pb-8"
|
||||
class="flex flex-col items-center px-4 py-16 lg:px-20 lg:pt-18 lg:pb-8"
|
||||
>
|
||||
<h1
|
||||
class="text-primary-comfy-canvas text-center text-5xl font-light whitespace-pre-line lg:text-8xl"
|
||||
|
||||
@@ -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>
|
||||
|
||||
152
apps/website/src/components/product/cloud/AIModelsSection.vue
Normal file
@@ -0,0 +1,152 @@
|
||||
<script setup lang="ts">
|
||||
import type { Locale } from '../../../i18n/translations'
|
||||
|
||||
import { externalLinks } from '../../../config/routes'
|
||||
import { t } from '../../../i18n/translations'
|
||||
import BrandButton from '../../common/BrandButton.vue'
|
||||
|
||||
type ModelCard = {
|
||||
titleKey:
|
||||
| 'cloud.aiModels.card.grokImagine'
|
||||
| 'cloud.aiModels.card.nanoBananaPro'
|
||||
| 'cloud.aiModels.card.ltx23'
|
||||
| 'cloud.aiModels.card.qwenImageEdit'
|
||||
| 'cloud.aiModels.card.wan22TextToVideo'
|
||||
imageSrc: string
|
||||
badgeIcon: string
|
||||
badgeClass: string
|
||||
layoutClass: string
|
||||
}
|
||||
|
||||
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
|
||||
const modelCards: ModelCard[] = [
|
||||
{
|
||||
titleKey: 'cloud.aiModels.card.grokImagine',
|
||||
imageSrc: '/images/cloud/ai-models/grok-imagine.webp',
|
||||
badgeIcon: '/icons/ai-models/grok.svg',
|
||||
badgeClass:
|
||||
'bg-white/20 text-white rounded-2xl backdrop-blur-sm group-hover:bg-primary-comfy-yellow group-hover:text-primary-comfy-ink',
|
||||
layoutClass: 'lg:col-span-6 lg:aspect-[16/7]'
|
||||
},
|
||||
{
|
||||
titleKey: 'cloud.aiModels.card.nanoBananaPro',
|
||||
imageSrc: '/images/cloud/ai-models/nano-banana-pro.webp',
|
||||
badgeIcon: '/icons/ai-models/gemini.svg',
|
||||
badgeClass:
|
||||
'bg-white/20 text-white rounded-full backdrop-blur-sm group-hover:bg-primary-comfy-yellow group-hover:text-primary-comfy-ink',
|
||||
layoutClass: 'lg:col-span-6 lg:aspect-[16/7]'
|
||||
},
|
||||
{
|
||||
titleKey: 'cloud.aiModels.card.ltx23',
|
||||
imageSrc: '/images/cloud/ai-models/ltx-23.webp',
|
||||
badgeIcon: '/icons/ai-models/ltx.svg',
|
||||
badgeClass:
|
||||
'bg-white/20 text-white rounded-full backdrop-blur-sm group-hover:bg-primary-comfy-yellow group-hover:text-primary-comfy-ink',
|
||||
layoutClass: 'lg:col-span-4 lg:aspect-[4/3]'
|
||||
},
|
||||
{
|
||||
titleKey: 'cloud.aiModels.card.qwenImageEdit',
|
||||
imageSrc: '/images/cloud/ai-models/qwen-image-edit.webp',
|
||||
badgeIcon: '/icons/ai-models/qwen.svg',
|
||||
badgeClass:
|
||||
'bg-white/20 text-white rounded-full backdrop-blur-sm group-hover:bg-primary-comfy-yellow group-hover:text-primary-comfy-ink',
|
||||
layoutClass: 'lg:col-span-4 lg:aspect-[4/3]'
|
||||
},
|
||||
{
|
||||
titleKey: 'cloud.aiModels.card.wan22TextToVideo',
|
||||
imageSrc: '/images/cloud/ai-models/wan-22.webp',
|
||||
badgeIcon: '/icons/ai-models/wan.svg',
|
||||
badgeClass:
|
||||
'bg-white/20 text-white rounded-full backdrop-blur-sm group-hover:bg-primary-comfy-yellow group-hover:text-primary-comfy-ink',
|
||||
layoutClass: 'lg:col-span-4 lg:aspect-[4/3]'
|
||||
}
|
||||
]
|
||||
|
||||
function getCardClass(layoutClass: string): string {
|
||||
return `${layoutClass} group relative h-72 overflow-hidden rounded-4xl bg-black/40 lg:h-auto cursor-pointer`
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="bg-primary-comfy-ink px-4 py-24 lg:px-20 lg:py-40">
|
||||
<div class="mx-auto flex w-full max-w-7xl flex-col items-center">
|
||||
<p
|
||||
class="text-primary-comfy-yellow text-center text-sm font-bold tracking-widest uppercase"
|
||||
>
|
||||
{{ t('cloud.aiModels.label', locale) }}
|
||||
</p>
|
||||
|
||||
<h2
|
||||
class="text-primary-comfy-canvas text-3.5xl/tight mt-8 max-w-4xl text-center font-light lg:text-5xl"
|
||||
>
|
||||
{{ t('cloud.aiModels.heading', locale) }}
|
||||
</h2>
|
||||
|
||||
<p
|
||||
class="text-primary-comfy-canvas mt-8 max-w-xl text-center text-sm font-light lg:text-base/snug"
|
||||
>
|
||||
{{ t('cloud.aiModels.subtitle', locale) }}
|
||||
</p>
|
||||
|
||||
<div class="mt-24 w-full">
|
||||
<div class="rounded-4xl border border-white/12 p-2 lg:p-1.5">
|
||||
<div class="grid grid-cols-1 gap-2 lg:grid-cols-12">
|
||||
<a
|
||||
v-for="card in modelCards"
|
||||
:key="card.titleKey"
|
||||
:href="externalLinks.workflows"
|
||||
:class="getCardClass(card.layoutClass)"
|
||||
>
|
||||
<img
|
||||
:src="card.imageSrc"
|
||||
alt=""
|
||||
class="size-full object-cover"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="absolute inset-0 bg-linear-to-t from-black/50 via-transparent to-black/15"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="absolute top-5 right-5 flex h-12 min-w-12 items-center justify-center px-3 lg:top-6 lg:right-6"
|
||||
:class="card.badgeClass"
|
||||
>
|
||||
<span
|
||||
class="inline-block size-6 bg-current"
|
||||
:style="{
|
||||
maskImage: `url(${card.badgeIcon})`,
|
||||
maskSize: 'contain',
|
||||
maskRepeat: 'no-repeat',
|
||||
maskPosition: 'center'
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p
|
||||
class="text-primary-warm-white absolute inset-x-6 bottom-6 text-5xl/tight font-light whitespace-pre-line lg:top-6 lg:right-auto lg:bottom-auto lg:text-4xl"
|
||||
>
|
||||
{{ t(card.titleKey, locale) }}
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BrandButton
|
||||
:href="externalLinks.workflows"
|
||||
variant="outline"
|
||||
class-name="mt-4 lg:mt-8 w-full max-w-md text-center text-sm lg:w-auto"
|
||||
>
|
||||
<span class="lg:hidden">{{
|
||||
t('cloud.aiModels.ctaMobile', locale)
|
||||
}}</span>
|
||||
<span class="hidden lg:inline">{{
|
||||
t('cloud.aiModels.ctaDesktop', locale)
|
||||
}}</span>
|
||||
</BrandButton>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -0,0 +1,70 @@
|
||||
<script setup lang="ts">
|
||||
import type { Locale } from '../../../i18n/translations'
|
||||
|
||||
import { t } from '../../../i18n/translations'
|
||||
|
||||
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
|
||||
const cards = [
|
||||
{
|
||||
labelKey: 'cloud.audience.creators.label' as const,
|
||||
titleKey: 'cloud.audience.creators.title' as const,
|
||||
descriptionKey: 'cloud.audience.creators.description' as const,
|
||||
image: '/images/cloud/audience-creator.jpg'
|
||||
},
|
||||
{
|
||||
labelKey: 'cloud.audience.teams.label' as const,
|
||||
titleKey: 'cloud.audience.teams.title' as const,
|
||||
descriptionKey: 'cloud.audience.teams.description' as const,
|
||||
image: '/images/cloud/audience-team.jpg'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="px-4 pt-24 lg:px-20 lg:pt-40">
|
||||
<h2
|
||||
class="text-primary-comfy-canvas text-3.5xl/tight mx-auto max-w-3xl text-center font-light lg:text-5xl/tight"
|
||||
>
|
||||
{{ t('cloud.audience.heading', locale).split('{creators}')[0]
|
||||
}}<span class="text-white">{{
|
||||
t('cloud.audience.headingHighlight', locale)
|
||||
}}</span
|
||||
>{{ t('cloud.audience.heading', locale).split('{creators}')[1] }}
|
||||
</h2>
|
||||
|
||||
<div
|
||||
class="rounded-5xl bg-transparency-white-t4 mt-12 grid grid-cols-1 gap-6 p-2 lg:mt-20 lg:grid-cols-2"
|
||||
>
|
||||
<div
|
||||
v-for="card in cards"
|
||||
:key="card.labelKey"
|
||||
class="bg-primary-comfy-ink rounded-4.5xl overflow-hidden"
|
||||
>
|
||||
<img
|
||||
:src="card.image"
|
||||
:alt="t(card.titleKey, locale)"
|
||||
class="aspect-4/3 w-full rounded-4xl object-cover"
|
||||
/>
|
||||
|
||||
<div class="mt-8 p-6">
|
||||
<p
|
||||
class="text-primary-comfy-yellow text-sm font-bold tracking-widest uppercase"
|
||||
>
|
||||
{{ t(card.labelKey, locale) }}
|
||||
</p>
|
||||
|
||||
<h3
|
||||
class="text-primary-comfy-canvas mt-8 text-3xl/tight font-light whitespace-pre-line"
|
||||
>
|
||||
{{ t(card.titleKey, locale) }}
|
||||
</h3>
|
||||
|
||||
<p class="text-primary-comfy-canvas mt-8 text-base/normal">
|
||||
{{ t(card.descriptionKey, locale) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
16
apps/website/src/components/product/cloud/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="cloud.faq.heading"
|
||||
faq-prefix="cloud.faq"
|
||||
:faq-count="15"
|
||||
/>
|
||||
</template>
|
||||
41
apps/website/src/components/product/cloud/HeroSection.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import type { Locale } from '../../../i18n/translations'
|
||||
|
||||
import { t } from '../../../i18n/translations'
|
||||
import BrandButton from '../../common/BrandButton.vue'
|
||||
import ProductHeroBadge from '../../common/ProductHeroBadge.vue'
|
||||
|
||||
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="flex flex-col items-center px-4 py-24 lg:px-20 lg:py-24">
|
||||
<ProductHeroBadge text="CLOUD" />
|
||||
|
||||
<h1
|
||||
class="text-primary-comfy-canvas text-3.5xl mt-8 text-center font-light whitespace-pre-line lg:text-5xl"
|
||||
>
|
||||
{{ t('cloud.hero.heading', locale) }}
|
||||
</h1>
|
||||
|
||||
<p
|
||||
class="mt-8 max-w-md text-center text-base/relaxed text-white/60 lg:mt-20 lg:max-w-lg"
|
||||
>
|
||||
{{ t('cloud.hero.subtitle', locale) }}
|
||||
</p>
|
||||
|
||||
<BrandButton
|
||||
href="https://www.comfy.org"
|
||||
:label="t('cloud.hero.cta', locale)"
|
||||
class="mt-12 w-full text-center lg:mt-8 lg:w-auto"
|
||||
/>
|
||||
|
||||
<div class="mt-12 w-full max-w-2xl lg:mt-16">
|
||||
<img
|
||||
src="/images/cloud/hero-cube.svg"
|
||||
alt="ComfyUI Cloud — 3D node cube illustration"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
41
apps/website/src/components/product/cloud/PricingSection.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import type { Locale } from '../../../i18n/translations'
|
||||
|
||||
import { getRoutes } from '../../../config/routes'
|
||||
import { t } from '../../../i18n/translations'
|
||||
|
||||
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section
|
||||
class="bg-transparency-white-t4 rounded-5xl mx-4 mt-4 mb-24 p-2 lg:mx-20 lg:mt-8 lg:mb-40"
|
||||
>
|
||||
<div
|
||||
class="bg-primary-comfy-yellow flex flex-col gap-24 rounded-4xl p-8 lg:flex-row lg:items-end lg:justify-between"
|
||||
>
|
||||
<div class="max-w-2xl">
|
||||
<h2
|
||||
class="text-primary-comfy-ink text-2xl/tight font-medium lg:text-3xl/tight"
|
||||
>
|
||||
{{ t('cloud.pricing.title', locale) }}
|
||||
</h2>
|
||||
|
||||
<p class="text-primary-comfy-ink mt-4 text-base">
|
||||
{{ t('cloud.pricing.description', locale) }}
|
||||
</p>
|
||||
|
||||
<p class="text-primary-comfy-ink mt-4 text-base font-bold">
|
||||
{{ t('cloud.pricing.tagline', locale) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<a
|
||||
:href="getRoutes(locale).cloudPricing"
|
||||
class="bg-primary-comfy-ink text-primary-comfy-yellow shrink-0 rounded-2xl px-6 py-3 text-center text-sm font-semibold transition-opacity hover:opacity-90"
|
||||
>
|
||||
{{ t('cloud.pricing.cta', locale) }}
|
||||
</a>
|
||||
</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="cloud"
|
||||
label-key="products.labelProducts"
|
||||
/>
|
||||
</template>
|
||||
55
apps/website/src/components/product/cloud/ReasonSection.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<script setup lang="ts">
|
||||
import type { Locale } from '../../../i18n/translations'
|
||||
import { t } from '../../../i18n/translations'
|
||||
|
||||
import type { Reason } from '../shared/ReasonSection.vue'
|
||||
|
||||
import ReasonSection from '../shared/ReasonSection.vue'
|
||||
|
||||
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
|
||||
interface CloudReason extends Reason {
|
||||
badge?: boolean
|
||||
}
|
||||
|
||||
const reasons: CloudReason[] = [
|
||||
{
|
||||
titleKey: 'cloud.reason.1.title',
|
||||
descriptionKey: 'cloud.reason.1.description'
|
||||
},
|
||||
{
|
||||
titleKey: 'cloud.reason.2.title',
|
||||
descriptionKey: 'cloud.reason.2.description',
|
||||
badge: true
|
||||
},
|
||||
{
|
||||
titleKey: 'cloud.reason.3.title',
|
||||
descriptionKey: 'cloud.reason.3.description'
|
||||
},
|
||||
{
|
||||
titleKey: 'cloud.reason.4.title',
|
||||
descriptionKey: 'cloud.reason.4.description'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ReasonSection
|
||||
:locale="locale"
|
||||
heading-key="cloud.reason.heading"
|
||||
heading-highlight-key="cloud.reason.headingHighlight"
|
||||
:reasons="reasons"
|
||||
>
|
||||
<template #reason-extra="{ reason }">
|
||||
<div v-if="(reason as CloudReason).badge" class="mt-3">
|
||||
<span
|
||||
class="font-formula-condensed text-primary-comfy-yellow text-lg font-bold tracking-wide"
|
||||
>
|
||||
{{ t('cloud.reason.2.badge.onlyOn', locale) }}
|
||||
<img src="/icons/logo.svg" alt="Comfy" class="inline-block h-5" />
|
||||
{{ t('cloud.reason.2.badge.cloud', locale) }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</ReasonSection>
|
||||
</template>
|
||||
@@ -0,0 +1,27 @@
|
||||
<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 p-4 text-center lg:px-20 lg:py-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,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
@@ -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
@@ -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>
|
||||
38
apps/website/src/components/product/local/ReasonSection.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import type { Locale } from '../../../i18n/translations'
|
||||
|
||||
import type { Reason } from '../shared/ReasonSection.vue'
|
||||
|
||||
import ReasonSection from '../shared/ReasonSection.vue'
|
||||
|
||||
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
|
||||
const reasons: Reason[] = [
|
||||
{
|
||||
titleKey: 'download.reason.1.title',
|
||||
descriptionKey: 'download.reason.1.description'
|
||||
},
|
||||
{
|
||||
titleKey: 'download.reason.2.title',
|
||||
descriptionKey: 'download.reason.2.description'
|
||||
},
|
||||
{
|
||||
titleKey: 'download.reason.3.title',
|
||||
descriptionKey: 'download.reason.3.description'
|
||||
},
|
||||
{
|
||||
titleKey: 'download.reason.4.title',
|
||||
descriptionKey: 'download.reason.4.description'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ReasonSection
|
||||
:locale="locale"
|
||||
heading-key="download.reason.heading"
|
||||
heading-highlight-key="download.reason.headingHighlight"
|
||||
highlight-class="text-primary-warm-white"
|
||||
:reasons="reasons"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import type { Reason } from './ReasonSection.vue'
|
||||
|
||||
import ReasonSection from './ReasonSection.vue'
|
||||
|
||||
const defaultReasons: Reason[] = [
|
||||
{
|
||||
titleKey: 'download.reason.1.title',
|
||||
descriptionKey: 'download.reason.1.description'
|
||||
},
|
||||
{
|
||||
titleKey: 'download.reason.2.title',
|
||||
descriptionKey: 'download.reason.2.description'
|
||||
},
|
||||
{
|
||||
titleKey: 'download.reason.3.title',
|
||||
descriptionKey: 'download.reason.3.description'
|
||||
},
|
||||
{
|
||||
titleKey: 'download.reason.4.title',
|
||||
descriptionKey: 'download.reason.4.description'
|
||||
}
|
||||
]
|
||||
|
||||
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: {
|
||||
headingKey: 'download.reason.heading',
|
||||
headingHighlightKey: 'download.reason.headingHighlight',
|
||||
highlightClass: 'text-primary-warm-white',
|
||||
reasons: defaultReasons
|
||||
}
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {}
|
||||
|
||||
export const FewReasons: Story = {
|
||||
args: {
|
||||
reasons: defaultReasons.slice(0, 2)
|
||||
}
|
||||
}
|
||||
65
apps/website/src/components/product/shared/ReasonSection.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<script setup lang="ts">
|
||||
import type { Locale, TranslationKey } from '../../../i18n/translations'
|
||||
|
||||
import { t } from '../../../i18n/translations'
|
||||
|
||||
export interface Reason {
|
||||
titleKey: TranslationKey
|
||||
descriptionKey: TranslationKey
|
||||
}
|
||||
|
||||
const {
|
||||
locale = 'en',
|
||||
headingKey,
|
||||
headingHighlightKey,
|
||||
highlightClass = 'text-white',
|
||||
reasons
|
||||
} = defineProps<{
|
||||
locale?: Locale
|
||||
headingKey: TranslationKey
|
||||
headingHighlightKey: TranslationKey
|
||||
highlightClass?: 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"
|
||||
>
|
||||
{{ t(headingKey, locale)
|
||||
}}<span :class="highlightClass">{{
|
||||
t(headingHighlightKey, locale)
|
||||
}}</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<!-- Right reasons list -->
|
||||
<div class="flex-1">
|
||||
<div
|
||||
v-for="reason in reasons"
|
||||
:key="reason.titleKey"
|
||||
class="border-primary-comfy-canvas/20 flex flex-col gap-4 border-b py-10 first:pt-0 md:flex-row md:gap-12"
|
||||
>
|
||||
<div class="shrink-0 md:w-52">
|
||||
<h3
|
||||
class="text-primary-comfy-canvas text-2xl font-light whitespace-pre-line"
|
||||
>
|
||||
{{ t(reason.titleKey, locale) }}
|
||||
</h3>
|
||||
<slot name="reason-extra" :reason="reason" />
|
||||
</div>
|
||||
<p class="text-primary-comfy-canvas/70 flex-1 text-sm">
|
||||
{{ t(reason.descriptionKey, locale) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
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() }
|
||||
}
|
||||
@@ -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,476 @@ const translations = {
|
||||
en: "Comfy gives you the building blocks to create workflows nobody's imagined yet — and share them with everyone.",
|
||||
'zh-CN': 'Comfy 为您提供构建模块,创造出前所未有的工作流——并与所有人分享。'
|
||||
},
|
||||
|
||||
// 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,同样的工作流,同样的结果,随时随地。'
|
||||
},
|
||||
// Cloud – HeroSection
|
||||
'cloud.hero.heading': {
|
||||
en: 'The full power of\nComfyUI — from\nanywhere.',
|
||||
'zh-CN': 'ComfyUI 的全部能力\n随时随地。'
|
||||
},
|
||||
'cloud.hero.subtitle': {
|
||||
en: 'The easiest way to start with ComfyUI. Pre-loaded models. Pre-installed custom nodes. Concurrent jobs. The full power of ComfyUI on Blackwell RTX 6000 Pros. Open a tab and start creating.',
|
||||
'zh-CN':
|
||||
'最简单的 ComfyUI 入门方式。预加载模型。预安装自定义节点。并发任务。在 Blackwell RTX 6000 Pro 上体验 ComfyUI 的全部能力。打开标签页,开始创作。'
|
||||
},
|
||||
'cloud.hero.cta': {
|
||||
en: 'TRY COMFY CLOUD FOR FREE',
|
||||
'zh-CN': '免费试用 COMFY CLOUD'
|
||||
},
|
||||
|
||||
// Cloud – ReasonSection
|
||||
'cloud.reason.heading': {
|
||||
en: 'Why\nprofessionals\nchoose ',
|
||||
'zh-CN': '专业人士为何\n选择'
|
||||
},
|
||||
'cloud.reason.headingHighlight': {
|
||||
en: 'Cloud',
|
||||
'zh-CN': 'Cloud'
|
||||
},
|
||||
'cloud.reason.1.title': {
|
||||
en: 'Powerful GPUs with end-\nto-end security built-in',
|
||||
'zh-CN': '强大 GPU\n端到端安全内置'
|
||||
},
|
||||
'cloud.reason.1.description': {
|
||||
en: 'Comfy Cloud works on any device. Pay only for running workflows, not idle time. With Comfy Cloud, you get security and infrastructure built-in with access to the most popular custom nodes.',
|
||||
'zh-CN':
|
||||
'Comfy Cloud 可在任何设备上使用。只需为运行工作流付费,无需为闲置时间付费。使用 Comfy Cloud,您可获得内置的安全性和基础设施,并访问最流行的自定义节点。'
|
||||
},
|
||||
'cloud.reason.2.title': {
|
||||
en: 'All models. Commercial\nlicense guaranteed.',
|
||||
'zh-CN': '所有模型。\n商业许可保证。'
|
||||
},
|
||||
'cloud.reason.2.description': {
|
||||
en: 'Run open-source models like Wan 2.2, Flux, LTX and Qwen alongside partner models like Nano Banana, Seedance, Seedream, Grok, Kling, Hunyuan 3D and more. Every model on Comfy Cloud is cleared for commercial use. No license ambiguity. All through one credit balance.',
|
||||
'zh-CN':
|
||||
'运行 Wan 2.2、Flux、LTX 和 Qwen 等开源模型,以及 Nano Banana、Seedance、Seedream、Grok、Kling、Hunyuan 3D 等合作伙伴模型。Comfy Cloud 上的每个模型都已获得商业使用许可。无许可证歧义。通过统一的积分余额使用。'
|
||||
},
|
||||
'cloud.reason.2.badge.onlyOn': {
|
||||
en: 'ONLY ON',
|
||||
'zh-CN': '仅在'
|
||||
},
|
||||
'cloud.reason.2.badge.cloud': {
|
||||
en: 'CLOUD',
|
||||
'zh-CN': '云端'
|
||||
},
|
||||
'cloud.reason.3.title': {
|
||||
en: 'More control than any\nother visual AI tool',
|
||||
'zh-CN': '比任何其他\n视觉 AI 工具更强的控制力'
|
||||
},
|
||||
'cloud.reason.3.description': {
|
||||
en: 'Every node exposed. Every setting adjustable. ComfyUI gives you the full inference pipeline. Choose your sampler, your scheduler, your model chain. The cloud simplifies the setup and supercharges the hardware underneath it.',
|
||||
'zh-CN':
|
||||
'每个节点都可见。每个设置都可调。ComfyUI 为您提供完整的推理管线。选择您的采样器、调度器、模型链。云端简化了设置并增强了底层硬件。'
|
||||
},
|
||||
'cloud.reason.4.title': {
|
||||
en: 'Community workflows,\nunlimited customization\nthrough pre-installed\ncustom nodes',
|
||||
'zh-CN': '社区工作流,\n通过预安装自定义节点\n实现无限自定义'
|
||||
},
|
||||
'cloud.reason.4.description': {
|
||||
en: 'Browse, run, and remix workflows built by thousands of creators. Start from proven templates instead of blank canvases. Upload custom LoRAs or finetuned foundational models from CivitAI and Hugging Face. The nodes powering ~90% of local ComfyUI workflows are now in the cloud.',
|
||||
'zh-CN':
|
||||
'浏览、运行和混搭由数千名创作者构建的工作流。从经过验证的模板开始,而非空白画布。上传自定义 LoRA 或来自 CivitAI 和 Hugging Face 的微调基础模型。驱动约 90% 本地 ComfyUI 工作流的节点现已上云。'
|
||||
},
|
||||
|
||||
// Cloud – AIModelsSection
|
||||
'cloud.aiModels.label': {
|
||||
en: 'AI MODELS',
|
||||
'zh-CN': 'AI 模型'
|
||||
},
|
||||
'cloud.aiModels.heading': {
|
||||
en: 'Run the world’s\nleading AI models',
|
||||
'zh-CN': '运行全球领先的\nAI 模型'
|
||||
},
|
||||
'cloud.aiModels.subtitle': {
|
||||
en: 'New models are added as they launch.',
|
||||
'zh-CN': '新模型发布后会第一时间上线。'
|
||||
},
|
||||
'cloud.aiModels.card.grokImagine': {
|
||||
en: 'Grok Imagine',
|
||||
'zh-CN': 'Grok Imagine'
|
||||
},
|
||||
'cloud.aiModels.card.nanoBananaPro': {
|
||||
en: 'Nano Banana Pro',
|
||||
'zh-CN': 'Nano Banana Pro'
|
||||
},
|
||||
'cloud.aiModels.card.ltx23': {
|
||||
en: 'LTX 2.3',
|
||||
'zh-CN': 'LTX 2.3'
|
||||
},
|
||||
'cloud.aiModels.card.qwenImageEdit': {
|
||||
en: 'Advanced image\nediting with Qwen',
|
||||
'zh-CN': 'Qwen 高级\n图像编辑'
|
||||
},
|
||||
'cloud.aiModels.card.wan22TextToVideo': {
|
||||
en: 'Wan 2.2\ntext to video',
|
||||
'zh-CN': 'Wan 2.2\n文生视频'
|
||||
},
|
||||
'cloud.aiModels.ctaDesktop': {
|
||||
en: 'EXPLORE WORKFLOWS WITH THE LATEST MODELS',
|
||||
'zh-CN': '探索最新模型工作流'
|
||||
},
|
||||
'cloud.aiModels.ctaMobile': {
|
||||
en: 'EXPLORE WORKFLOWS',
|
||||
'zh-CN': '探索工作流'
|
||||
},
|
||||
|
||||
// Cloud – AudienceSection
|
||||
'cloud.audience.heading': {
|
||||
en: 'Built for {creators} who need quality, control, and simplicity.',
|
||||
'zh-CN': '为追求质量、控制与简约的{creators}而生。'
|
||||
},
|
||||
'cloud.audience.headingHighlight': {
|
||||
en: 'creators',
|
||||
'zh-CN': '创作者'
|
||||
},
|
||||
'cloud.audience.creators.label': {
|
||||
en: 'CREATORS',
|
||||
'zh-CN': '创作者'
|
||||
},
|
||||
'cloud.audience.creators.title': {
|
||||
en: 'From idea to output\nin minutes.',
|
||||
'zh-CN': '从创意到成品,\n只需几分钟。'
|
||||
},
|
||||
'cloud.audience.creators.description': {
|
||||
en: 'For those who want to generate images, video, and 3D with more control than prompt-based tools offer — without spending a weekend configuring your machine.',
|
||||
'zh-CN':
|
||||
'适合那些想要生成图像、视频和 3D 内容,且需要比基于提示词的工具更精细控制的人——无需花一整个周末配置机器。'
|
||||
},
|
||||
'cloud.audience.teams.label': {
|
||||
en: 'TEAMS & STUDIOS',
|
||||
'zh-CN': '团队与工作室'
|
||||
},
|
||||
'cloud.audience.teams.title': {
|
||||
en: 'Onboard your\nteam today.',
|
||||
'zh-CN': '立即开始\n团队协作。'
|
||||
},
|
||||
'cloud.audience.teams.description': {
|
||||
en: 'No IT setup, no GPU procurement, no environment headaches. Everyone works from the same up-to-date platform. Share workflows via App Mode links — your team runs them instantly, no training required.',
|
||||
'zh-CN':
|
||||
'无需 IT 部署、GPU 采购,也没有环境配置的烦恼。所有人在同一个最新平台上工作。通过 App Mode 链接分享工作流——团队立即运行,无需培训。'
|
||||
},
|
||||
|
||||
// Cloud – PricingSection
|
||||
'cloud.pricing.title': {
|
||||
en: 'Simple, credit-based pricing',
|
||||
'zh-CN': '简单的按积分计费'
|
||||
},
|
||||
'cloud.pricing.description': {
|
||||
en: 'One balance for Cloud GPU time and Partner Node API models. Build and edit workflows for free — credits are consumed only when the GPU runs.',
|
||||
'zh-CN':
|
||||
'一个余额即可使用云端 GPU 算力和合作伙伴节点 API 模型。免费构建和编辑工作流——仅在 GPU 运行时消耗积分。'
|
||||
},
|
||||
'cloud.pricing.tagline': {
|
||||
en: "Start free. Upgrade when you're ready.",
|
||||
'zh-CN': '免费开始,随时升级。'
|
||||
},
|
||||
'cloud.pricing.cta': {
|
||||
en: 'SEE PRICING PLANS',
|
||||
'zh-CN': '查看定价方案'
|
||||
},
|
||||
|
||||
// Cloud – FAQSection
|
||||
'cloud.faq.heading': {
|
||||
en: "FAQ's",
|
||||
'zh-CN': '常见问题'
|
||||
},
|
||||
'cloud.faq.1.q': {
|
||||
en: 'What is Comfy Cloud / ComfyUI Cloud?',
|
||||
'zh-CN': '什么是 Comfy Cloud / ComfyUI Cloud?'
|
||||
},
|
||||
'cloud.faq.1.a': {
|
||||
en: 'Comfy Cloud is a version of ComfyUI that we officially host — no setup, no GPU required. Run your workflows instantly on high-performance cloud GPUs.',
|
||||
'zh-CN':
|
||||
'Comfy Cloud 是我们官方托管的 ComfyUI 版本——无需设置,无需 GPU。在高性能云端 GPU 上即时运行您的工作流。'
|
||||
},
|
||||
'cloud.faq.2.q': {
|
||||
en: 'How is Cloud different from running ComfyUI locally?',
|
||||
'zh-CN': 'Cloud 与本地运行 ComfyUI 有什么区别?'
|
||||
},
|
||||
'cloud.faq.2.a': {
|
||||
en: 'Cloud runs on powerful remote GPUs and is accessible from any device. Local runs entirely on your computer, giving you full control and offline use.',
|
||||
'zh-CN':
|
||||
'Cloud 在强大的远程 GPU 上运行,可从任何设备访问。本地版完全在您的电脑上运行,提供完全控制和离线使用。'
|
||||
},
|
||||
'cloud.faq.3.q': {
|
||||
en: 'Which version should I choose, Comfy Cloud or local ComfyUI (self-hosted)?',
|
||||
'zh-CN': '我应该选择 Comfy Cloud 还是本地 ComfyUI(自托管)?'
|
||||
},
|
||||
'cloud.faq.3.a': {
|
||||
en: "Comfy Cloud (beta) has zero setup, is easy to share with your team, and is faster than most GPUs you can run on a desktop workstation. You can immediately run the best models and workflows from the community on Comfy Cloud.\nLocal ComfyUI is infinitely customizable, works offline, and you don't need to worry about queue times. However, depending on what you want to create, you might need to have a good GPU and some amount of technical knowledge to install community-created custom nodes.",
|
||||
'zh-CN':
|
||||
'Comfy Cloud(测试版)无需任何设置,方便与团队共享,比大多数桌面工作站 GPU 更快。您可以立即在 Comfy Cloud 上运行社区中最好的模型和工作流。\n本地 ComfyUI 可以无限定制,支持离线工作,无需担心排队时间。但根据您的创作需求,可能需要一块好的 GPU 以及一定的技术知识来安装社区创建的自定义节点。'
|
||||
},
|
||||
'cloud.faq.4.q': {
|
||||
en: 'Do I need a GPU or a strong computer to use Comfy Cloud?',
|
||||
'zh-CN': '使用 Comfy Cloud 需要 GPU 或高性能电脑吗?'
|
||||
},
|
||||
'cloud.faq.4.a': {
|
||||
en: 'No, you can start creating instantly from your browser, no matter what computer you use.',
|
||||
'zh-CN': '不需要,无论使用什么电脑,您都可以从浏览器即时开始创作。'
|
||||
},
|
||||
'cloud.faq.5.q': {
|
||||
en: 'What machine or GPU does Comfy Cloud run on?',
|
||||
'zh-CN': 'Comfy Cloud 使用什么机器或 GPU?'
|
||||
},
|
||||
'cloud.faq.5.a': {
|
||||
en: 'Comfy Cloud runs on Blackwell RTX 6000 Pros — 96GB VRAM.',
|
||||
'zh-CN': 'Comfy Cloud 运行在 Blackwell RTX 6000 Pro 上——96GB 显存。'
|
||||
},
|
||||
'cloud.faq.6.q': {
|
||||
en: 'Can I use my existing workflows with Comfy Cloud?',
|
||||
'zh-CN': '我可以在 Comfy Cloud 上使用现有的工作流吗?'
|
||||
},
|
||||
'cloud.faq.6.a': {
|
||||
en: 'Yes, your workflows work across Local and Cloud. Just note that only the most popular custom nodes are supported for now, but more will be added soon.',
|
||||
'zh-CN':
|
||||
'可以,您的工作流在本地和云端都能使用。请注意,目前仅支持最热门的自定义节点,但很快会添加更多。'
|
||||
},
|
||||
'cloud.faq.7.q': {
|
||||
en: 'Are all ComfyUI extensions and custom nodes supported?',
|
||||
'zh-CN': '所有 ComfyUI 扩展和自定义节点都支持吗?'
|
||||
},
|
||||
'cloud.faq.7.a': {
|
||||
en: 'You can always check Cloud to see the list of extensions and models that we support, for free.\nMost popular ones are available, and new ones are added over time.',
|
||||
'zh-CN':
|
||||
'您可以随时在 Cloud 上免费查看我们支持的扩展和模型列表。\n大多数热门扩展已可用,新的扩展会持续添加。'
|
||||
},
|
||||
'cloud.faq.8.q': {
|
||||
en: 'Can I use my own models or checkpoints?',
|
||||
'zh-CN': '我可以使用自己的模型或检查点吗?'
|
||||
},
|
||||
'cloud.faq.8.a': {
|
||||
en: 'You can always check Cloud to see the list of extensions and models that we support, for free.\nCurrently, we support a wide variety of preinstalled models.\nFor those on the Creator or Pro plans, you can bring in your own fine-tuned LoRAs from CivitAI to perfect your own style.\nImporting from HuggingFace and direct file upload for larger models is on our roadmap.',
|
||||
'zh-CN':
|
||||
'您可以随时在 Cloud 上免费查看我们支持的扩展和模型列表。\n目前我们支持大量预装模型。\n对于 Creator 或 Pro 计划用户,您可以导入自己从 CivitAI 微调的 LoRA 来打造专属风格。\n从 HuggingFace 导入和大型模型的直接上传功能已在我们的路线图中。'
|
||||
},
|
||||
'cloud.faq.9.q': {
|
||||
en: 'Can I run long or multiple workflows?',
|
||||
'zh-CN': '我可以运行长时间或多个工作流吗?'
|
||||
},
|
||||
'cloud.faq.9.a': {
|
||||
en: "Each workflow can run for up to 30 minutes, with one active job at a time. We're adding higher tiers and parallel runs soon for even more flexibility.",
|
||||
'zh-CN':
|
||||
'每个工作流最长可运行 30 分钟,同时运行一个活跃任务。我们即将推出更高层级和并行运行,提供更大灵活性。'
|
||||
},
|
||||
'cloud.faq.10.q': {
|
||||
en: 'How is my user data stored and secured in Comfy Cloud?',
|
||||
'zh-CN': '我的用户数据在 Comfy Cloud 中如何存储和保护?'
|
||||
},
|
||||
'cloud.faq.10.a': {
|
||||
en: 'By default, all your inputs, outputs, and workflows are private to your account.\nFor enhanced security features or enterprise-level options, please contact our team via support@comfy.org for more details.',
|
||||
'zh-CN':
|
||||
'默认情况下,您的所有输入、输出和工作流都是您账户的私有数据。\n如需增强安全功能或企业级选项,请通过 support@comfy.org 联系我们的团队了解更多详情。'
|
||||
},
|
||||
'cloud.faq.11.q': {
|
||||
en: 'How does pricing for Comfy Cloud work?',
|
||||
'zh-CN': 'Comfy Cloud 的定价是怎样的?'
|
||||
},
|
||||
'cloud.faq.11.a': {
|
||||
en: 'Your monthly plan grants you a single credit balance that you can spend anywhere. Partner Nodes (formerly API nodes) will have set credit prices per usage. For Cloud workflows, you will be charged credits based on the exact duration of your workflow run — longer runs consume more credits.',
|
||||
'zh-CN':
|
||||
'您的月度计划会授予一个可在任何地方使用的积分余额。合作伙伴节点(原 API 节点)按使用量设定积分价格。对于云端工作流,将根据工作流运行的确切时长收取积分——运行时间越长消耗的积分越多。'
|
||||
},
|
||||
'cloud.faq.12.q': {
|
||||
en: "What's the difference between Partner Node credits and my Cloud subscription?",
|
||||
'zh-CN': '合作伙伴节点积分和我的 Cloud 订阅有什么区别?'
|
||||
},
|
||||
'cloud.faq.12.a': {
|
||||
en: 'Comfy Cloud has a credit system that is used for both Partner nodes (formerly API nodes) and running workflows on cloud.\n1. Partner Nodes (Pay-as-you-go): These nodes (formerly called API nodes) run third-party models via API calls and can be used on both Comfy Cloud and Local/Self-Hosted ComfyUI. Each node has its own usage cost, determined by the API provider, and we directly match their pricing.\n2. Running workflows on cloud: Exclusive to Comfy Cloud, you get a set amount of credits per month, with the amount differing based on your plan. More credits can be topped up anytime. Credits are only used up for GPU time while workflows are running — not while editing or building them. No idle costs, no setup, and no infrastructure to manage.',
|
||||
'zh-CN':
|
||||
'Comfy Cloud 有一个积分系统,用于合作伙伴节点(原 API 节点)和在云端运行工作流。\n1. 合作伙伴节点(按需付费):这些节点(原称 API 节点)通过 API 调用运行第三方模型,可在 Comfy Cloud 和本地/自托管 ComfyUI 上使用。每个节点有其自身的使用成本,由 API 提供商决定,我们直接匹配他们的定价。\n2. 在云端运行工作流:Comfy Cloud 专属,您每月获得一定数量的积分,数量根据您的计划而不同。积分可随时充值。积分仅在工作流运行时用于 GPU 时间——编辑或构建时不消耗。无闲置成本,无需设置,无需管理基础设施。'
|
||||
},
|
||||
'cloud.faq.13.q': {
|
||||
en: 'Can I cancel my subscription?',
|
||||
'zh-CN': '我可以取消订阅吗?'
|
||||
},
|
||||
'cloud.faq.13.a': {
|
||||
en: "Yes. You can cancel your subscription anytime through your account's billing settings, powered by Stripe. Your plan will remain active until the end of your current billing period.",
|
||||
'zh-CN':
|
||||
'可以。您可以随时通过账户的账单设置取消订阅(由 Stripe 提供支持)。您的计划将在当前计费周期结束前保持有效。'
|
||||
},
|
||||
'cloud.faq.14.q': {
|
||||
en: "Where can I find my invoices or add my company's tax ID?",
|
||||
'zh-CN': '我在哪里可以找到发票或添加公司税号?'
|
||||
},
|
||||
'cloud.faq.14.a': {
|
||||
en: "You can manage all billing details directly through your Stripe portal.\nGo to Settings → Plans & Credits → Invoice History to open the Stripe portal. From there, you can view and download invoices, update your billing information, and add your company's tax ID.",
|
||||
'zh-CN':
|
||||
'您可以通过 Stripe 门户直接管理所有账单详情。\n前往设置 → 计划与积分 → 发票历史以打开 Stripe 门户。在那里,您可以查看和下载发票、更新账单信息并添加公司税号。'
|
||||
},
|
||||
'cloud.faq.15.q': {
|
||||
en: 'Will ComfyUI always be free to run locally?',
|
||||
'zh-CN': 'ComfyUI 本地运行会一直免费吗?'
|
||||
},
|
||||
'cloud.faq.15.a': {
|
||||
en: "Yes, absolutely. ComfyUI will always be free and open source. You can deploy it however you want, such as downloading it from GitHub, using Docker, custom setups, etc.\n\nComfy Cloud is an optional hosted service for those who prefer convenience, accessibility, or don't have powerful GPUs.",
|
||||
'zh-CN':
|
||||
'是的,绝对如此。ComfyUI 将始终免费且开源。您可以按任何方式部署它,例如从 GitHub 下载、使用 Docker、自定义设置等。\n\nComfy Cloud 是一项可选的托管服务,适合偏好便捷性、可访问性或没有强大 GPU 的用户。'
|
||||
},
|
||||
|
||||
'buildWhat.row1': { en: 'BUILD WHAT', 'zh-CN': '构建' },
|
||||
'buildWhat.row2a': { en: "DOESN'T EXIST", 'zh-CN': '尚不存在的' },
|
||||
'buildWhat.row2b': { en: 'YET', 'zh-CN': '事物' },
|
||||
@@ -278,4 +752,4 @@ export function t(key: TranslationKey, locale: Locale = 'en'): string {
|
||||
return translations[key][locale] ?? translations[key].en
|
||||
}
|
||||
|
||||
export type { Locale }
|
||||
export type { Locale, TranslationKey }
|
||||
|
||||
@@ -119,7 +119,7 @@ const websiteJsonLd = {
|
||||
)}
|
||||
|
||||
<SiteNav locale={locale} client:load />
|
||||
<main>
|
||||
<main class="mt-20 lg:mt-32">
|
||||
<slot />
|
||||
</main>
|
||||
<SiteFooter locale={locale} client:load />
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro'
|
||||
import ComingSoon from '../../components/common/ComingSoon.astro'
|
||||
import HeroSection from '../../components/product/cloud/HeroSection.vue'
|
||||
import ReasonSection from '../../components/product/cloud/ReasonSection.vue'
|
||||
import AIModelsSection from '../../components/product/cloud/AIModelsSection.vue'
|
||||
import AudienceSection from '../../components/product/cloud/AudienceSection.vue'
|
||||
import PricingSection from '../../components/product/cloud/PricingSection.vue'
|
||||
import ProductCardsSection from '../../components/product/cloud/ProductCardsSection.vue'
|
||||
import FAQSection from '../../components/product/cloud/FAQSection.vue'
|
||||
---
|
||||
|
||||
<BaseLayout title="Comfy Cloud — Run AI From Anywhere">
|
||||
<ComingSoon />
|
||||
<BaseLayout title="Comfy Cloud — AI in the Cloud">
|
||||
<HeroSection />
|
||||
<ReasonSection />
|
||||
<AIModelsSection />
|
||||
<AudienceSection />
|
||||
<PricingSection />
|
||||
<ProductCardsSection />
|
||||
<FAQSection client:visible />
|
||||
</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/local/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>
|
||||
|
||||
@@ -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/local/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>
|
||||
|
||||
@@ -62,8 +62,10 @@
|
||||
--color-transparency-white-t4: rgb(255 255 255 / 0.04);
|
||||
--font-formula: 'PP Formula', sans-serif;
|
||||
--font-formula-condensed: 'PP Formula Condensed', sans-serif;
|
||||
--text-3\.5xl: 2rem;
|
||||
--text-6\.5xl: 4.125rem;
|
||||
--radius-4xl: 2rem;
|
||||
--radius-4\.5xl: 2.25rem;
|
||||
--radius-5xl: 2.5rem;
|
||||
}
|
||||
|
||||
@@ -113,3 +115,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
@@ -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"]
|
||||
}
|
||||