mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 06:20:11 +00:00
fix: update for reviews
This commit is contained in:
@@ -6,9 +6,7 @@ test.describe('Homepage @smoke', () => {
|
||||
})
|
||||
|
||||
test('has correct title', async ({ page }) => {
|
||||
await expect(page).toHaveTitle(
|
||||
'Comfy — Professional Control of Visual AI'
|
||||
)
|
||||
await expect(page).toHaveTitle('Comfy — Professional Control of Visual AI')
|
||||
})
|
||||
|
||||
test('HeroSection heading is visible', async ({ page }) => {
|
||||
@@ -24,9 +22,7 @@ test.describe('Homepage @smoke', () => {
|
||||
})
|
||||
|
||||
test('ProductShowcase section is visible', async ({ page }) => {
|
||||
await expect(
|
||||
page.getByText('HOW', { exact: true }).first()
|
||||
).toBeVisible()
|
||||
await expect(page.getByText('HOW', { exact: true }).first()).toBeVisible()
|
||||
await expect(
|
||||
page.getByText(/Connect models, processing steps, and outputs/)
|
||||
).toBeVisible()
|
||||
@@ -63,6 +59,7 @@ test.describe('Homepage @smoke', () => {
|
||||
})
|
||||
|
||||
test('BuildWhatSection is visible', async ({ page }) => {
|
||||
// "DOESN'T EXIST" is the actual badge text rendered in the Build What section
|
||||
await expect(page.getByText("DOESN'T EXIST")).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -13,18 +13,22 @@ test.describe('Desktop navigation @smoke', () => {
|
||||
|
||||
test('has all top-level nav items', async ({ page }) => {
|
||||
const nav = page.getByRole('navigation', { name: 'Main navigation' })
|
||||
const desktopLinks = nav.locator('.md\\:flex').first()
|
||||
const desktopLinks = nav.getByTestId('desktop-nav-links')
|
||||
|
||||
for (const label of ['PRODUCTS', 'PRICING', 'COMMUNITY', 'RESOURCES', 'COMPANY']) {
|
||||
await expect(
|
||||
desktopLinks.getByText(label).first()
|
||||
).toBeVisible()
|
||||
for (const label of [
|
||||
'PRODUCTS',
|
||||
'PRICING',
|
||||
'COMMUNITY',
|
||||
'RESOURCES',
|
||||
'COMPANY'
|
||||
]) {
|
||||
await expect(desktopLinks.getByText(label).first()).toBeVisible()
|
||||
}
|
||||
})
|
||||
|
||||
test('CTA buttons are visible', async ({ page }) => {
|
||||
const nav = page.getByRole('navigation', { name: 'Main navigation' })
|
||||
const desktopCTA = nav.locator('.md\\:flex').last()
|
||||
const desktopCTA = nav.getByTestId('desktop-nav-cta')
|
||||
await expect(
|
||||
desktopCTA.getByRole('link', { name: 'DOWNLOAD LOCAL' })
|
||||
).toBeVisible()
|
||||
@@ -41,15 +45,13 @@ test.describe('Desktop dropdown @interaction', () => {
|
||||
|
||||
test('hovering PRODUCTS shows dropdown items', async ({ page }) => {
|
||||
const nav = page.getByRole('navigation', { name: 'Main navigation' })
|
||||
const desktopLinks = nav.locator('.md\\:flex').first()
|
||||
const desktopLinks = nav.getByTestId('desktop-nav-links')
|
||||
const productsButton = desktopLinks.getByRole('button', {
|
||||
name: /PRODUCTS/i
|
||||
})
|
||||
await productsButton.hover()
|
||||
|
||||
const dropdown = productsButton
|
||||
.locator('..')
|
||||
.locator('.backdrop-blur-md')
|
||||
const dropdown = productsButton.locator('..').getByTestId('nav-dropdown')
|
||||
for (const item of [
|
||||
'Comfy Local',
|
||||
'Comfy Cloud',
|
||||
@@ -62,7 +64,7 @@ test.describe('Desktop dropdown @interaction', () => {
|
||||
|
||||
test('moving mouse away closes dropdown', async ({ page }) => {
|
||||
const nav = page.getByRole('navigation', { name: 'Main navigation' })
|
||||
const desktopLinks = nav.locator('.md\\:flex').first()
|
||||
const desktopLinks = nav.getByTestId('desktop-nav-links')
|
||||
await desktopLinks.getByRole('button', { name: /PRODUCTS/i }).hover()
|
||||
|
||||
const comfyLocal = nav.getByRole('link', { name: 'Comfy Local' }).first()
|
||||
@@ -74,7 +76,7 @@ test.describe('Desktop dropdown @interaction', () => {
|
||||
|
||||
test('Escape key closes dropdown', async ({ page }) => {
|
||||
const nav = page.getByRole('navigation', { name: 'Main navigation' })
|
||||
const desktopLinks = nav.locator('.md\\:flex').first()
|
||||
const desktopLinks = nav.getByTestId('desktop-nav-links')
|
||||
await desktopLinks.getByRole('button', { name: /PRODUCTS/i }).hover()
|
||||
|
||||
const comfyLocal = nav.getByRole('link', { name: 'Comfy Local' }).first()
|
||||
@@ -131,9 +133,7 @@ test.describe('Mobile menu @mobile', () => {
|
||||
await expect(
|
||||
menu.getByRole('link', { name: 'DOWNLOAD LOCAL' })
|
||||
).toBeVisible()
|
||||
await expect(
|
||||
menu.getByRole('link', { name: 'LAUNCH CLOUD' })
|
||||
).toBeVisible()
|
||||
await expect(menu.getByRole('link', { name: 'LAUNCH CLOUD' })).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -155,7 +155,7 @@ test.describe('Footer @smoke', () => {
|
||||
|
||||
test('copyright text is visible', async ({ page }) => {
|
||||
await expect(
|
||||
page.locator('footer').getByText(/© 2026 Comfy Org/)
|
||||
page.locator('footer').getByText(/© \d{4} Comfy Org/)
|
||||
).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,13 +7,11 @@ test.describe('Desktop layout @smoke', () => {
|
||||
|
||||
test('navigation links visible and hamburger hidden', async ({ page }) => {
|
||||
const nav = page.getByRole('navigation', { name: 'Main navigation' })
|
||||
const desktopLinks = nav.locator('.md\\:flex').first()
|
||||
const desktopLinks = nav.getByTestId('desktop-nav-links')
|
||||
await expect(desktopLinks.getByText('PRODUCTS').first()).toBeVisible()
|
||||
await expect(desktopLinks.getByText('PRICING').first()).toBeVisible()
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Toggle menu' })
|
||||
).toBeHidden()
|
||||
await expect(page.getByRole('button', { name: 'Toggle menu' })).toBeHidden()
|
||||
})
|
||||
|
||||
test('product cards in grid layout', async ({ page }) => {
|
||||
@@ -44,7 +42,7 @@ test.describe('Mobile layout @mobile', () => {
|
||||
})
|
||||
|
||||
test('SocialProofBar shows two marquee rows on mobile', async ({ page }) => {
|
||||
const mobileContainer = page.locator('.flex.flex-col.gap-8.md\\:hidden')
|
||||
const mobileContainer = page.getByTestId('social-proof-mobile')
|
||||
await expect(mobileContainer).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
Binary file not shown.
BIN
apps/website/public/fonts/PPFormula-Bold.woff2
Normal file
BIN
apps/website/public/fonts/PPFormula-Bold.woff2
Normal file
Binary file not shown.
Binary file not shown.
BIN
apps/website/public/fonts/PPFormula-CondensedSemibold.woff2
Normal file
BIN
apps/website/public/fonts/PPFormula-CondensedSemibold.woff2
Normal file
Binary file not shown.
Binary file not shown.
BIN
apps/website/public/fonts/PPFormula-Light.woff2
Normal file
BIN
apps/website/public/fonts/PPFormula-Light.woff2
Normal file
Binary file not shown.
Binary file not shown.
BIN
apps/website/public/fonts/PPFormula-Medium.woff2
Normal file
BIN
apps/website/public/fonts/PPFormula-Medium.woff2
Normal file
Binary file not shown.
Binary file not shown.
BIN
apps/website/public/fonts/PPFormula-Regular.woff2
Normal file
BIN
apps/website/public/fonts/PPFormula-Regular.woff2
Normal file
Binary file not shown.
Binary file not shown.
BIN
apps/website/public/fonts/PPFormula-Semibold.woff2
Normal file
BIN
apps/website/public/fonts/PPFormula-Semibold.woff2
Normal file
Binary file not shown.
@@ -5,7 +5,7 @@ export interface FooterLink {
|
||||
external?: boolean
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
const { title, links } = defineProps<{
|
||||
title: string
|
||||
links: FooterLink[]
|
||||
}>()
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onUnmounted, watch } from 'vue'
|
||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
import type { Locale } from '../../i18n/translations'
|
||||
import { t } from '../../i18n/translations'
|
||||
import BrandButton from './BrandButton.vue'
|
||||
import type { NavLink } from './NavDesktopLink.vue'
|
||||
|
||||
@@ -13,20 +15,20 @@ interface CtaLink {
|
||||
const {
|
||||
open = false,
|
||||
links = [],
|
||||
ctaLinks = []
|
||||
ctaLinks = [],
|
||||
locale = 'en'
|
||||
} = defineProps<{
|
||||
open?: boolean
|
||||
links?: NavLink[]
|
||||
ctaLinks?: CtaLink[]
|
||||
locale?: Locale
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
}>()
|
||||
|
||||
const activeSection = defineModel<string | null>('activeSection', {
|
||||
default: null
|
||||
})
|
||||
const activeSection = ref<string | null>(null)
|
||||
|
||||
const activeSectionItems = computed(
|
||||
() => links.find((l) => l.label === activeSection.value)?.items
|
||||
@@ -97,7 +99,7 @@ onUnmounted(() => {
|
||||
@click="activeSection = null"
|
||||
>
|
||||
<span aria-hidden="true">‹</span>
|
||||
BACK
|
||||
{{ t('nav.back', locale) }}
|
||||
</button>
|
||||
|
||||
<p class="text-primary-warm-gray mb-4 text-sm">
|
||||
|
||||
@@ -34,11 +34,14 @@ const emit = defineEmits<{
|
||||
class="relative"
|
||||
@mouseenter="link.items?.length && emit('open', link.label)"
|
||||
@mouseleave="emit('close')"
|
||||
@focusin="link.items?.length && emit('open', link.label)"
|
||||
@focusout="emit('close')"
|
||||
>
|
||||
<button
|
||||
v-if="link.items?.length"
|
||||
type="button"
|
||||
class="group text-primary-comfy-canvas hover:text-primary-warm-gray flex cursor-pointer items-center gap-1.5 py-3 text-sm font-bold tracking-wide uppercase transition-colors"
|
||||
aria-haspopup="true"
|
||||
:aria-expanded="isOpen"
|
||||
@click="emit('toggle', link.label)"
|
||||
>
|
||||
@@ -63,12 +66,15 @@ const emit = defineEmits<{
|
||||
<div
|
||||
v-if="link.items?.length"
|
||||
v-show="isOpen"
|
||||
role="menu"
|
||||
data-testid="nav-dropdown"
|
||||
class="bg-transparency-white-t4 absolute top-full left-0 w-max rounded-xl p-2 shadow-lg backdrop-blur-md"
|
||||
>
|
||||
<a
|
||||
v-for="item in link.items"
|
||||
:key="item.href"
|
||||
:href="item.href"
|
||||
role="menuitem"
|
||||
class="text-primary-comfy-canvas hover:bg-transparency-white-t4 flex items-center gap-2 rounded-sm p-2 text-xs font-medium tracking-wide transition-colors hover:text-white"
|
||||
@click="emit('close')"
|
||||
>
|
||||
|
||||
@@ -40,11 +40,31 @@ const topColumns: { title: string; links: FooterLink[] }[] = [
|
||||
{
|
||||
title: t('footer.resources', locale),
|
||||
links: [
|
||||
{ label: t('footer.blog', locale), href: externalLinks.blog, external: true },
|
||||
{ label: t('nav.discord', locale), href: externalLinks.discord, external: true },
|
||||
{ label: t('nav.github', locale), href: externalLinks.github, external: true },
|
||||
{ label: t('nav.docs', locale), href: externalLinks.docs, external: true },
|
||||
{ label: t('nav.youtube', locale), href: externalLinks.youtube, external: true }
|
||||
{
|
||||
label: t('footer.blog', locale),
|
||||
href: externalLinks.blog,
|
||||
external: true
|
||||
},
|
||||
{
|
||||
label: t('nav.discord', locale),
|
||||
href: externalLinks.discord,
|
||||
external: true
|
||||
},
|
||||
{
|
||||
label: t('nav.github', locale),
|
||||
href: externalLinks.github,
|
||||
external: true
|
||||
},
|
||||
{
|
||||
label: t('nav.docs', locale),
|
||||
href: externalLinks.docs,
|
||||
external: true
|
||||
},
|
||||
{
|
||||
label: t('nav.youtube', locale),
|
||||
href: externalLinks.youtube,
|
||||
external: true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -67,7 +87,10 @@ const contactSection = {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer ref="footerRef" class="bg-primary-comfy-ink text-primary-comfy-canvas px-6 py-8 lg:px-20">
|
||||
<footer
|
||||
ref="footerRef"
|
||||
class="bg-primary-comfy-ink text-primary-comfy-canvas px-6 py-8 lg:px-20"
|
||||
>
|
||||
<div
|
||||
class="border-primary-warm-gray flex flex-col gap-12 border-t pt-16 lg:gap-0"
|
||||
>
|
||||
@@ -75,9 +98,7 @@ const contactSection = {
|
||||
<div class="flex flex-col gap-12 lg:flex-row lg:gap-0">
|
||||
<!-- Left: tagline -->
|
||||
<div class="flex-1">
|
||||
<p
|
||||
class="text-2xl font-medium tracking-wide uppercase lg:text-3xl"
|
||||
>
|
||||
<p class="text-2xl font-medium tracking-wide uppercase lg:text-3xl">
|
||||
{{ t('footer.tagline', locale) }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -139,14 +160,13 @@ const contactSection = {
|
||||
</div>
|
||||
|
||||
<!-- Logo + bottom bar -->
|
||||
<div class="flex flex-col gap-8 lg:flex-row lg:items-end lg:justify-between">
|
||||
<canvas
|
||||
ref="canvasRef"
|
||||
class="mt-12 size-52 opacity-80 lg:mt-24"
|
||||
/>
|
||||
<div
|
||||
class="flex flex-col gap-8 lg:flex-row lg:items-end lg:justify-between"
|
||||
>
|
||||
<canvas ref="canvasRef" class="mt-12 size-52 opacity-80 lg:mt-24" />
|
||||
<div class="flex justify-center gap-6 lg:justify-end">
|
||||
<p class="text-sm">{{ t('footer.location', locale) }}</p>
|
||||
<p class="text-sm">© 2026 Comfy Org</p>
|
||||
<p class="text-sm">© {{ new Date().getFullYear() }} Comfy Org</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -18,7 +18,11 @@ const navLinks: NavLink[] = [
|
||||
items: [
|
||||
{ label: t('nav.comfyLocal', locale), href: routes.download },
|
||||
{ label: t('nav.comfyCloud', locale), href: routes.cloud },
|
||||
{ label: t('nav.comfyApi', locale), href: routes.api, badge: 'NEW' },
|
||||
{
|
||||
label: t('nav.comfyApi', locale),
|
||||
href: routes.api,
|
||||
badge: t('nav.badgeNew', locale)
|
||||
},
|
||||
{ label: t('nav.comfyEnterprise', locale), href: routes.cloudEnterprise }
|
||||
]
|
||||
},
|
||||
@@ -26,18 +30,42 @@ const navLinks: NavLink[] = [
|
||||
{
|
||||
label: t('nav.community', locale),
|
||||
items: [
|
||||
{ label: t('nav.comfyHub', locale), href: externalLinks.workflows, badge: 'NEW' },
|
||||
{
|
||||
label: t('nav.comfyHub', locale),
|
||||
href: externalLinks.workflows,
|
||||
badge: t('nav.badgeNew', locale)
|
||||
},
|
||||
{ label: t('nav.gallery', locale), href: routes.gallery }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: t('nav.resources', locale),
|
||||
items: [
|
||||
{ label: t('nav.blogs', locale), href: externalLinks.blog, external: true },
|
||||
{ label: t('nav.github', locale), href: externalLinks.github, external: true },
|
||||
{ label: t('nav.discord', locale), href: externalLinks.discord, external: true },
|
||||
{ label: t('nav.docs', locale), href: externalLinks.docs, external: true },
|
||||
{ label: t('nav.youtube', locale), href: externalLinks.youtube, external: true }
|
||||
{
|
||||
label: t('nav.blogs', locale),
|
||||
href: externalLinks.blog,
|
||||
external: true
|
||||
},
|
||||
{
|
||||
label: t('nav.github', locale),
|
||||
href: externalLinks.github,
|
||||
external: true
|
||||
},
|
||||
{
|
||||
label: t('nav.discord', locale),
|
||||
href: externalLinks.discord,
|
||||
external: true
|
||||
},
|
||||
{
|
||||
label: t('nav.docs', locale),
|
||||
href: externalLinks.docs,
|
||||
external: true
|
||||
},
|
||||
{
|
||||
label: t('nav.youtube', locale),
|
||||
href: externalLinks.youtube,
|
||||
external: true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -51,8 +79,16 @@ const navLinks: NavLink[] = [
|
||||
]
|
||||
|
||||
const ctaButtons = [
|
||||
{ label: t('nav.downloadLocal', locale), href: routes.download, primary: false },
|
||||
{ label: t('nav.launchCloud', locale), href: externalLinks.app, primary: true }
|
||||
{
|
||||
label: t('nav.downloadLocal', locale),
|
||||
href: routes.download,
|
||||
primary: false
|
||||
},
|
||||
{
|
||||
label: t('nav.launchCloud', locale),
|
||||
href: externalLinks.app,
|
||||
primary: true
|
||||
}
|
||||
]
|
||||
|
||||
const currentPath = ref('')
|
||||
@@ -102,18 +138,20 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MobileMenu :open="mobileMenuOpen" :links="navLinks" :cta-links="ctaButtons" @close="closeMobileMenu" />
|
||||
<MobileMenu
|
||||
:open="mobileMenuOpen"
|
||||
:links="navLinks"
|
||||
:cta-links="ctaButtons"
|
||||
:locale="locale"
|
||||
@close="closeMobileMenu"
|
||||
/>
|
||||
|
||||
<nav
|
||||
class="bg-primary-comfy-ink fixed inset-x-0 top-0 z-50 flex items-center justify-between px-6 py-5 md:px-20 md:py-8"
|
||||
aria-label="Main navigation"
|
||||
>
|
||||
<a :href="routes.home" aria-label="Comfy home">
|
||||
<img
|
||||
src="/icons/logomark.svg"
|
||||
alt="Comfy"
|
||||
class="h-8 md:hidden"
|
||||
/>
|
||||
<img src="/icons/logomark.svg" alt="Comfy" class="h-8 md:hidden" />
|
||||
<span
|
||||
class="hidden h-10 w-36 bg-contain bg-left bg-no-repeat md:block"
|
||||
style="background-image: url('/icons/logo.svg')"
|
||||
@@ -122,7 +160,10 @@ onUnmounted(() => {
|
||||
</a>
|
||||
|
||||
<!-- Desktop nav links -->
|
||||
<div class="hidden items-center gap-10 md:flex">
|
||||
<div
|
||||
data-testid="desktop-nav-links"
|
||||
class="hidden items-center gap-10 md:flex"
|
||||
>
|
||||
<NavDesktopLink
|
||||
v-for="link in navLinks"
|
||||
:key="link.label"
|
||||
@@ -136,7 +177,10 @@ onUnmounted(() => {
|
||||
</div>
|
||||
|
||||
<!-- Desktop CTA buttons -->
|
||||
<div class="hidden items-center gap-2 md:flex">
|
||||
<div
|
||||
data-testid="desktop-nav-cta"
|
||||
class="hidden items-center gap-2 md:flex"
|
||||
>
|
||||
<BrandButton
|
||||
v-for="cta in ctaButtons"
|
||||
:key="cta.href"
|
||||
@@ -150,9 +194,11 @@ onUnmounted(() => {
|
||||
<!-- Mobile hamburger -->
|
||||
<button
|
||||
class="flex size-10 items-center justify-center rounded-xl md:hidden"
|
||||
:class="mobileMenuOpen
|
||||
? 'border-primary-comfy-yellow border-2 bg-transparent'
|
||||
: 'bg-primary-comfy-yellow'"
|
||||
:class="
|
||||
mobileMenuOpen
|
||||
? 'border-primary-comfy-yellow border-2 bg-transparent'
|
||||
: 'bg-primary-comfy-yellow'
|
||||
"
|
||||
:aria-label="t('nav.toggleMenu', locale)"
|
||||
aria-controls="site-mobile-menu"
|
||||
:aria-expanded="mobileMenuOpen"
|
||||
|
||||
@@ -6,8 +6,11 @@ import NodeBadge from '../common/NodeBadge.vue'
|
||||
|
||||
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
|
||||
const row1 = [{ text: 'BUILD WHAT' }]
|
||||
const row2 = [{ text: "DOESN'T EXIST" }, { text: 'YET' }]
|
||||
const row1 = [{ text: t('buildWhat.row1', locale) }]
|
||||
const row2 = [
|
||||
{ text: t('buildWhat.row2a', locale) },
|
||||
{ text: t('buildWhat.row2b', locale) }
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -25,10 +25,10 @@ const routes = getRoutes(locale)
|
||||
>
|
||||
{{ t('caseStudy.label', locale) }}
|
||||
</p>
|
||||
<h2 class="text-primary-comfy-canvas text-5xl font-light">
|
||||
{{ t('caseStudy.heading', locale).split('\n')[0] }}<br />{{
|
||||
t('caseStudy.heading', locale).split('\n')[1]
|
||||
}}
|
||||
<h2
|
||||
class="text-primary-comfy-canvas text-5xl font-light whitespace-pre-line"
|
||||
>
|
||||
{{ t('caseStudy.heading', locale) }}
|
||||
</h2>
|
||||
<p class="text-primary-warm-gray text-base">
|
||||
{{ t('caseStudy.subheading', locale) }}
|
||||
|
||||
@@ -4,7 +4,7 @@ const { title, description } = defineProps<{
|
||||
description: string
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
const emit = defineEmits<{
|
||||
click: []
|
||||
}>()
|
||||
</script>
|
||||
@@ -12,7 +12,7 @@ defineEmits<{
|
||||
<template>
|
||||
<button
|
||||
class="rounded-5xl bg-primary-comfy-yellow text-primary-comfy-ink w-full cursor-pointer p-8 text-left transition-all"
|
||||
@click="$emit('click')"
|
||||
@click="emit('click')"
|
||||
>
|
||||
<h3 class="text-2xl/tight font-medium">
|
||||
{{ title }}
|
||||
|
||||
@@ -27,11 +27,9 @@ useFrameScrub(canvasRef, {
|
||||
class="flex flex-col items-center px-4 pt-36 pb-16 lg:px-20 lg:pt-50 lg:pb-8"
|
||||
>
|
||||
<h1
|
||||
class="text-primary-comfy-canvas text-center text-5xl font-light lg:text-8xl"
|
||||
class="text-primary-comfy-canvas text-center text-5xl font-light whitespace-pre-line lg:text-8xl"
|
||||
>
|
||||
{{ t('hero.title', locale).split('\n')[0] }}
|
||||
<br />
|
||||
{{ t('hero.title', locale).split('\n')[1] }}
|
||||
{{ t('hero.title', locale) }}
|
||||
</h1>
|
||||
|
||||
<div class="mt-12 w-full max-w-3xl overflow-hidden rounded-2xl">
|
||||
|
||||
@@ -50,11 +50,9 @@ const cards = [
|
||||
{{ t('products.label', locale) }}
|
||||
</p>
|
||||
<h2
|
||||
class="text-primary-comfy-canvas mt-4 text-4xl font-light lg:text-5xl"
|
||||
class="text-primary-comfy-canvas mt-4 text-4xl font-light whitespace-pre-line lg:text-5xl"
|
||||
>
|
||||
{{ t('products.heading', locale).split('\n')[0] }}
|
||||
<br />
|
||||
{{ t('products.heading', locale).split('\n')[1] }}
|
||||
{{ t('products.heading', locale) }}
|
||||
</h2>
|
||||
<p class="text-primary-warm-gray mt-4 text-sm">
|
||||
{{ t('products.subheading', locale) }}
|
||||
|
||||
@@ -28,9 +28,9 @@ const features = [
|
||||
]
|
||||
|
||||
const badgeSegments = [
|
||||
{ text: 'HOW' },
|
||||
{ text: t('showcase.badgeHow', locale) },
|
||||
{ logoSrc: '/icons/logo.svg', logoAlt: 'Comfy' },
|
||||
{ text: 'WORKS' }
|
||||
{ text: t('showcase.badgeWorks', locale) }
|
||||
]
|
||||
|
||||
const activeIndex = ref(0)
|
||||
|
||||
@@ -24,7 +24,7 @@ const row2 = logos.slice(6)
|
||||
<div class="animate-marquee hidden items-center gap-2 md:flex">
|
||||
<div
|
||||
v-for="(logo, i) in [...logos, ...logos]"
|
||||
:key="i"
|
||||
:key="`${logo}-${i}`"
|
||||
class="flex h-20 w-50 shrink-0 items-center justify-center"
|
||||
>
|
||||
<img :src="`/icons/clients/${logo}.svg`" :alt="logo" />
|
||||
@@ -32,11 +32,14 @@ const row2 = logos.slice(6)
|
||||
</div>
|
||||
|
||||
<!-- Two rows on mobile -->
|
||||
<div class="flex flex-col gap-8 md:hidden">
|
||||
<div
|
||||
data-testid="social-proof-mobile"
|
||||
class="flex flex-col gap-8 md:hidden"
|
||||
>
|
||||
<div class="animate-marquee flex items-center gap-8">
|
||||
<div
|
||||
v-for="(logo, i) in [...row1, ...row1]"
|
||||
:key="i"
|
||||
:key="`${logo}-${i}`"
|
||||
class="flex h-14 w-40 shrink-0 items-center justify-center"
|
||||
>
|
||||
<img :src="`/icons/clients/${logo}.svg`" :alt="logo" />
|
||||
@@ -45,7 +48,7 @@ const row2 = logos.slice(6)
|
||||
<div class="animate-marquee-reverse flex items-center gap-8">
|
||||
<div
|
||||
v-for="(logo, i) in [...row2, ...row2]"
|
||||
:key="i"
|
||||
:key="`${logo}-${i}`"
|
||||
class="flex h-14 w-40 shrink-0 items-center justify-center"
|
||||
>
|
||||
<img :src="`/icons/clients/${logo}.svg`" :alt="logo" />
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import type { Locale } from '../../i18n/translations'
|
||||
import { t } from '../../i18n/translations'
|
||||
import { gsap, ScrollTrigger } from '../../scripts/smoothScroll'
|
||||
import { gsap, ScrollTrigger } from '../../scripts/gsapSetup'
|
||||
|
||||
import { externalLinks } from '../../config/routes'
|
||||
import BrandButton from '../common/BrandButton.vue'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Ref } from 'vue'
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
import { gsap, ScrollTrigger } from '../scripts/smoothScroll'
|
||||
import { gsap, ScrollTrigger } from '../scripts/gsapSetup'
|
||||
|
||||
interface FrameScrubOptions {
|
||||
frameCount: number
|
||||
@@ -13,13 +13,14 @@ export function useFrameScrub(
|
||||
options: FrameScrubOptions
|
||||
) {
|
||||
let ctx: gsap.Context | undefined
|
||||
const images: HTMLImageElement[] = []
|
||||
|
||||
onMounted(() => {
|
||||
if (!canvasRef.value) return
|
||||
const canvas: HTMLCanvasElement = canvasRef.value
|
||||
|
||||
const context = canvas.getContext('2d')!
|
||||
const images: HTMLImageElement[] = []
|
||||
const context = canvas.getContext('2d')
|
||||
if (!context) return
|
||||
let loadedCount = 0
|
||||
const resolvedTrigger = options.scrollTrigger(canvas)
|
||||
|
||||
@@ -32,16 +33,19 @@ export function useFrameScrub(
|
||||
context.drawImage(img, 0, 0)
|
||||
}
|
||||
|
||||
function onFrameReady() {
|
||||
loadedCount++
|
||||
if (loadedCount === options.frameCount) {
|
||||
drawFrame(0)
|
||||
initScrub()
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < options.frameCount; i++) {
|
||||
const img = new Image()
|
||||
img.src = options.frameSrc(i)
|
||||
img.onload = () => {
|
||||
loadedCount++
|
||||
if (loadedCount === options.frameCount) {
|
||||
drawFrame(0)
|
||||
initScrub()
|
||||
}
|
||||
}
|
||||
img.onload = onFrameReady
|
||||
img.onerror = onFrameReady
|
||||
images.push(img)
|
||||
}
|
||||
|
||||
@@ -62,6 +66,11 @@ export function useFrameScrub(
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
images.forEach((img) => {
|
||||
img.onload = null
|
||||
img.onerror = null
|
||||
})
|
||||
images.length = 0
|
||||
ctx?.revert()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -49,6 +49,8 @@ const translations = {
|
||||
'zh-CN':
|
||||
'浏览和混搭数千个社区共享的工作流。从经过验证的模板开始,按需自定义。'
|
||||
},
|
||||
'showcase.badgeHow': { en: 'HOW', 'zh-CN': '如何' },
|
||||
'showcase.badgeWorks': { en: 'WORKS', 'zh-CN': '运作' },
|
||||
|
||||
// UseCaseSection
|
||||
'useCase.label': {
|
||||
@@ -214,9 +216,11 @@ const translations = {
|
||||
// BuildWhatSection
|
||||
'buildWhat.subtitle': {
|
||||
en: "Comfy gives you the building blocks to create workflows nobody's imagined yet — and share them with everyone.",
|
||||
'zh-CN':
|
||||
'Comfy 为您提供构建模块,创造出前所未有的工作流——并与所有人分享。'
|
||||
'zh-CN': 'Comfy 为您提供构建模块,创造出前所未有的工作流——并与所有人分享。'
|
||||
},
|
||||
'buildWhat.row1': { en: 'BUILD WHAT', 'zh-CN': '构建' },
|
||||
'buildWhat.row2a': { en: "DOESN'T EXIST", 'zh-CN': '尚不存在的' },
|
||||
'buildWhat.row2b': { en: 'YET', 'zh-CN': '事物' },
|
||||
|
||||
// SiteNav
|
||||
'nav.products': { en: 'PRODUCTS', 'zh-CN': '产品' },
|
||||
@@ -245,6 +249,7 @@ const translations = {
|
||||
'nav.launchCloud': { en: 'LAUNCH CLOUD', 'zh-CN': '启动云端' },
|
||||
'nav.toggleMenu': { en: 'Toggle menu', 'zh-CN': '切换菜单' },
|
||||
'nav.back': { en: 'BACK', 'zh-CN': '返回' },
|
||||
'nav.badgeNew': { en: 'NEW', 'zh-CN': '新' },
|
||||
|
||||
// SiteFooter
|
||||
'footer.tagline': {
|
||||
|
||||
@@ -3,5 +3,14 @@ import BaseLayout from '../layouts/BaseLayout.astro'
|
||||
---
|
||||
|
||||
<BaseLayout title="About Us — Comfy">
|
||||
<!-- TODO: Add page content -->
|
||||
<section class="flex min-h-[60vh] items-center justify-center px-6">
|
||||
<div class="text-center">
|
||||
<h1 class="text-primary-comfy-canvas text-4xl font-light">
|
||||
Coming Soon
|
||||
</h1>
|
||||
<p class="text-primary-warm-gray mt-4 text-sm">
|
||||
This page is being redesigned. Check back soon.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
|
||||
@@ -3,5 +3,14 @@ import BaseLayout from '../layouts/BaseLayout.astro'
|
||||
---
|
||||
|
||||
<BaseLayout title="Careers — Comfy">
|
||||
<!-- TODO: Add page content -->
|
||||
<section class="flex min-h-[60vh] items-center justify-center px-6">
|
||||
<div class="text-center">
|
||||
<h1 class="text-primary-comfy-canvas text-4xl font-light">
|
||||
Coming Soon
|
||||
</h1>
|
||||
<p class="text-primary-warm-gray mt-4 text-sm">
|
||||
This page is being redesigned. Check back soon.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
|
||||
@@ -3,5 +3,14 @@ import BaseLayout from '../layouts/BaseLayout.astro'
|
||||
---
|
||||
|
||||
<BaseLayout title="Download Comfy — Run AI Locally">
|
||||
<!-- TODO: Add page content -->
|
||||
<section class="flex min-h-[60vh] items-center justify-center px-6">
|
||||
<div class="text-center">
|
||||
<h1 class="text-primary-comfy-canvas text-4xl font-light">
|
||||
Coming Soon
|
||||
</h1>
|
||||
<p class="text-primary-warm-gray mt-4 text-sm">
|
||||
This page is being redesigned. Check back soon.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
|
||||
@@ -3,5 +3,14 @@ import BaseLayout from '../layouts/BaseLayout.astro'
|
||||
---
|
||||
|
||||
<BaseLayout title="Gallery — Comfy">
|
||||
<!-- TODO: Add page content -->
|
||||
<section class="flex min-h-[60vh] items-center justify-center px-6">
|
||||
<div class="text-center">
|
||||
<h1 class="text-primary-comfy-canvas text-4xl font-light">
|
||||
Coming Soon
|
||||
</h1>
|
||||
<p class="text-primary-warm-gray mt-4 text-sm">
|
||||
This page is being redesigned. Check back soon.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
|
||||
6
apps/website/src/scripts/gsapSetup.ts
Normal file
6
apps/website/src/scripts/gsapSetup.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import gsap from 'gsap'
|
||||
import { ScrollTrigger } from 'gsap/ScrollTrigger'
|
||||
|
||||
gsap.registerPlugin(ScrollTrigger)
|
||||
|
||||
export { gsap, ScrollTrigger }
|
||||
@@ -1,30 +1,21 @@
|
||||
import gsap from 'gsap'
|
||||
import { ScrollTrigger } from 'gsap/ScrollTrigger'
|
||||
import Lenis from 'lenis'
|
||||
import { gsap, ScrollTrigger } from './gsapSetup'
|
||||
|
||||
gsap.registerPlugin(ScrollTrigger)
|
||||
let initialized = false
|
||||
|
||||
export async function initSmoothScroll() {
|
||||
if (initialized) return
|
||||
initialized = true
|
||||
|
||||
function needsLenis(): boolean {
|
||||
const ua = navigator.userAgent
|
||||
const isWindows = /Windows/.test(ua)
|
||||
const isLinux = /Linux/.test(ua) && !/Android/.test(ua)
|
||||
return isWindows || isLinux
|
||||
}
|
||||
if (!isWindows && !isLinux) return
|
||||
|
||||
let lenis: Lenis | undefined
|
||||
|
||||
export function initSmoothScroll() {
|
||||
if (lenis) return
|
||||
|
||||
if (!needsLenis()) return
|
||||
|
||||
lenis = new Lenis()
|
||||
const { default: Lenis } = await import('lenis')
|
||||
|
||||
const lenis = new Lenis()
|
||||
lenis.on('scroll', ScrollTrigger.update)
|
||||
|
||||
gsap.ticker.add((time) => {
|
||||
lenis!.raf(time * 1000)
|
||||
})
|
||||
gsap.ticker.add((time) => lenis.raf(time * 1000))
|
||||
gsap.ticker.lagSmoothing(0)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'PP Formula';
|
||||
src: url('/fonts/PPFormula-Light.otf') format('opentype');
|
||||
src: url('/fonts/PPFormula-Light.woff2') format('woff2');
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'PP Formula';
|
||||
src: url('/fonts/PPFormula-Regular.otf') format('opentype');
|
||||
src: url('/fonts/PPFormula-Regular.woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'PP Formula';
|
||||
src: url('/fonts/PPFormula-Medium.otf') format('opentype');
|
||||
src: url('/fonts/PPFormula-Medium.woff2') format('woff2');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'PP Formula';
|
||||
src: url('/fonts/PPFormula-Semibold.otf') format('opentype');
|
||||
src: url('/fonts/PPFormula-Semibold.woff2') format('woff2');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -35,7 +35,7 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'PP Formula';
|
||||
src: url('/fonts/PPFormula-Bold.otf') format('opentype');
|
||||
src: url('/fonts/PPFormula-Bold.woff2') format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -43,7 +43,7 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'PP Formula Condensed';
|
||||
src: url('/fonts/PPFormula-CondensedSemibold.otf') format('opentype');
|
||||
src: url('/fonts/PPFormula-CondensedSemibold.woff2') format('woff2');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
|
||||
Reference in New Issue
Block a user