mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 06:20:11 +00:00
fix: address PR review comments — a11y, i18n, security, DRY
- Fix SSO card showing wrong cloud copy in translations - Add type="button" to FAQ toggles, v-show for ARIA panel linkage - Add noreferrer to all target="_blank" links - Use externalLinks.app instead of hardcoded URL in cloud hero - Add contact route and use locale-aware routing for /contact - Fix "FAQ's" → "FAQs" typo in download and cloud sections - Remove lazy-loading from transition images to prevent pop-in - Mark decorative side images with empty alt + aria-hidden - Single-pass heading split in AudienceSection - Move cardDef above usage for readability - Export Feature interface for consumer type safety - Extract shared badge class constant in AIModelsSection Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-019d9393-67e1-75e9-9a86-9b1b1b7ef1ee
This commit is contained in:
@@ -59,6 +59,7 @@ function toggle(index: number) {
|
||||
>
|
||||
<button
|
||||
:id="`faq-trigger-${index}`"
|
||||
type="button"
|
||||
:aria-expanded="expanded[index]"
|
||||
:aria-controls="`faq-panel-${index}`"
|
||||
:class="
|
||||
@@ -89,8 +90,9 @@ function toggle(index: number) {
|
||||
</span>
|
||||
</button>
|
||||
<section
|
||||
v-if="expanded[index]"
|
||||
v-show="expanded[index]"
|
||||
:id="`faq-panel-${index}`"
|
||||
role="region"
|
||||
:aria-labelledby="`faq-trigger-${index}`"
|
||||
class="pb-6"
|
||||
>
|
||||
|
||||
@@ -21,13 +21,6 @@ const {
|
||||
|
||||
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,
|
||||
@@ -39,6 +32,13 @@ function cardDef(product: Product, href: string, bg: string) {
|
||||
}
|
||||
}
|
||||
|
||||
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')
|
||||
]
|
||||
|
||||
const cards = excludeProduct
|
||||
? allCards.filter((c) => c.product !== excludeProduct)
|
||||
: allCards
|
||||
|
||||
@@ -92,10 +92,9 @@ useParallax([leftImgRef], { trigger: sectionRef, y: -60 })
|
||||
<img
|
||||
:key="activeLeft"
|
||||
:src="activeLeft"
|
||||
:alt="activeLabel"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
class="absolute inset-0 size-full object-cover"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</Transition>
|
||||
</div>
|
||||
@@ -108,10 +107,9 @@ useParallax([leftImgRef], { trigger: sectionRef, y: -60 })
|
||||
<img
|
||||
:key="activeRight"
|
||||
:src="activeRight"
|
||||
:alt="activeLabel"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
class="absolute inset-0 size-full object-cover"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
@@ -32,7 +32,7 @@ const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
<a
|
||||
:href="externalLinks.app"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
rel="noopener noreferrer"
|
||||
class="bg-primary-comfy-yellow text-primary-comfy-ink rounded-full px-8 py-4 text-center text-sm font-bold tracking-wider transition-opacity hover:opacity-90 lg:min-w-60"
|
||||
>
|
||||
{{ t('api.hero.getApiKeys', locale) }}
|
||||
@@ -40,7 +40,7 @@ const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
<a
|
||||
:href="externalLinks.docs"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
rel="noopener noreferrer"
|
||||
class="border-primary-comfy-yellow text-primary-comfy-yellow hover:bg-primary-comfy-yellow hover:text-primary-comfy-ink rounded-full border px-8 py-4 text-center text-sm font-bold tracking-wider transition-colors lg:min-w-60"
|
||||
>
|
||||
{{ t('api.hero.viewDocs', locale) }}
|
||||
|
||||
@@ -22,45 +22,43 @@ type ModelCard = {
|
||||
|
||||
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
|
||||
const badgeBase =
|
||||
'bg-white/20 text-white backdrop-blur-sm group-hover:bg-primary-comfy-yellow group-hover:text-primary-comfy-ink'
|
||||
|
||||
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',
|
||||
badgeClass: `${badgeBase} rounded-2xl`,
|
||||
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',
|
||||
badgeClass: `${badgeBase} rounded-full`,
|
||||
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',
|
||||
badgeClass: `${badgeBase} rounded-full`,
|
||||
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',
|
||||
badgeClass: `${badgeBase} rounded-full`,
|
||||
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',
|
||||
badgeClass: `${badgeBase} rounded-full`,
|
||||
layoutClass: 'lg:col-span-4 lg:aspect-[4/3]'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -5,6 +5,8 @@ import { t } from '../../../i18n/translations'
|
||||
|
||||
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
|
||||
const headingParts = t('cloud.audience.heading', locale).split('{creators}')
|
||||
|
||||
const cards = [
|
||||
{
|
||||
labelKey: 'cloud.audience.creators.label' as const,
|
||||
@@ -26,11 +28,11 @@ const cards = [
|
||||
<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]
|
||||
{{ headingParts[0]
|
||||
}}<span class="text-white">{{
|
||||
t('cloud.audience.headingHighlight', locale)
|
||||
}}</span
|
||||
>{{ t('cloud.audience.heading', locale).split('{creators}')[1] }}
|
||||
>{{ headingParts[1] }}
|
||||
</h2>
|
||||
|
||||
<div
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<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'
|
||||
import ProductHeroBadge from '../../common/ProductHeroBadge.vue'
|
||||
@@ -25,7 +26,7 @@ const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
</p>
|
||||
|
||||
<BrandButton
|
||||
href="https://www.comfy.org"
|
||||
:href="externalLinks.app"
|
||||
:label="t('cloud.hero.cta', locale)"
|
||||
class="mt-12 w-full text-center lg:mt-8 lg:w-auto"
|
||||
/>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import type { Locale } from '../../../i18n/translations'
|
||||
|
||||
import { getRoutes } from '../../../config/routes'
|
||||
import { t } from '../../../i18n/translations'
|
||||
import ProductHeroBadge from '../../common/ProductHeroBadge.vue'
|
||||
|
||||
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
|
||||
const routes = getRoutes(locale)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -29,7 +32,7 @@ const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
class="mt-10 flex w-full max-w-md flex-col lg:w-auto lg:max-w-none"
|
||||
>
|
||||
<a
|
||||
href="/contact"
|
||||
:href="routes.contact"
|
||||
class="bg-primary-comfy-yellow text-primary-comfy-ink rounded-full px-8 py-4 text-center text-sm font-bold tracking-wider transition-opacity hover:opacity-90 lg:min-w-60"
|
||||
>
|
||||
{{ t('enterprise.hero.contactSales', locale) }}
|
||||
|
||||
@@ -24,7 +24,7 @@ const features = [
|
||||
title: t('enterprise.team.feature3.title', locale),
|
||||
description: t('enterprise.team.feature3.description', locale),
|
||||
ctaText: t('enterprise.hero.contactSales', locale),
|
||||
ctaHref: '/contact'
|
||||
ctaHref: routes.contact
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
@@ -34,7 +34,7 @@ const { downloadUrl } = useDownloadUrl()
|
||||
<a
|
||||
:href="downloadUrl"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
rel="noopener noreferrer"
|
||||
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) }}
|
||||
@@ -42,7 +42,7 @@ const { downloadUrl } = useDownloadUrl()
|
||||
<a
|
||||
:href="externalLinks.github"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
rel="noopener noreferrer"
|
||||
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
|
||||
|
||||
@@ -14,7 +14,8 @@ const baseRoutes = {
|
||||
termsOfService: '/terms-of-service',
|
||||
privacyPolicy: '/privacy-policy',
|
||||
videos: '/videos',
|
||||
caseStudies: '/case-studies'
|
||||
caseStudies: '/case-studies',
|
||||
contact: '/contact'
|
||||
} as const
|
||||
|
||||
type Routes = typeof baseRoutes
|
||||
|
||||
@@ -261,13 +261,12 @@ const translations = {
|
||||
'zh-CN': '单点登录'
|
||||
},
|
||||
'enterprise.team.feature2.description': {
|
||||
en: 'Most things you build locally run on Comfy Cloud \u2014 same file, same results, powerful GPUs on demand. When a job outgrows your machine, push it to the cloud. No conversion, no rework.',
|
||||
'zh-CN':
|
||||
'你在本地构建的大部分内容都能在 Comfy Cloud 上运行——相同文件、相同结果、按需使用强大 GPU。当任务超出你的机器能力时,推送到云端。无需转换,无需返工。'
|
||||
en: 'Enable secure, centralized user authentication across your organization with SSO and SCIM provisioning.',
|
||||
'zh-CN': '为您的组织启用集中式安全用户认证,支持 SSO 和 SCIM 配置。'
|
||||
},
|
||||
'enterprise.team.feature2.cta': {
|
||||
en: 'SEE CLOUD FEATURES',
|
||||
'zh-CN': '查看云端特性'
|
||||
en: 'SET UP SSO',
|
||||
'zh-CN': '设置 SSO'
|
||||
},
|
||||
'enterprise.team.feature3.title': {
|
||||
en: 'App Mode',
|
||||
@@ -548,7 +547,7 @@ const translations = {
|
||||
|
||||
// Download – FAQSection
|
||||
'download.faq.heading': {
|
||||
en: "FAQ's",
|
||||
en: 'FAQs',
|
||||
'zh-CN': '常见问题'
|
||||
},
|
||||
'download.faq.1.q': {
|
||||
@@ -878,7 +877,7 @@ const translations = {
|
||||
|
||||
// Cloud – FAQSection
|
||||
'cloud.faq.heading': {
|
||||
en: "FAQ's",
|
||||
en: 'FAQs',
|
||||
'zh-CN': '常见问题'
|
||||
},
|
||||
'cloud.faq.1.q': {
|
||||
|
||||
Reference in New Issue
Block a user