mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-19 02:06:38 +00:00
## Summary - Migrate the website's top nav from bespoke components (`SiteNav`, `MobileMenu`, `NavDesktopLink`, `PillButton`, `MaskRevealButton`) to shadcn-vue primitives (`NavigationMenu`, `Sheet`, `Button`), split into `HeaderMain` → `HeaderMainDesktop` + `HeaderMainMobile`. - Mobile nav becomes a `Sheet` with drill-down sub-navigation, scroll lock, sticky CTAs, sr-only i18n title/description, and a back-to-home logo. - Desktop nav uses `NavigationMenu` with shared viewport, featured cards, `NavColumn` extraction, and centralized nav data in `data/mainNavigation.ts`. - Adds i18n strings for nav labels, dropdown column headers, close/back affordances, and the mobile menu description. ## Test plan - [ ] Desktop: hover PRODUCTS / COMMUNITY / COMPANY — dropdowns open with featured card + columns, NEW badges render, external links show the arrow-up-right icon, viewport is shared between triggers. - [ ] Mobile (<lg): open hamburger sheet — verify logo + close, body scroll is locked while open, top-level nav scrolls if it overflows, CTAs stay pinned at bottom. - [ ] Tap COMMUNITY / PRODUCTS / COMPANY — sub-panel slides over root nav, in-sheet BACK returns to root, links navigate correctly. - [ ] Reload `?locale=zh-CN`: dropdown labels, sheet title/description, BACK / close affordances all localized. - [ ] No regressions on `/cloud`, `/cloud/pricing`, `/customers`, etc. — pages that swap CTAs (`BrandButton` → shadcn `Button`) still render at every breakpoint. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: GitHub Action <action@github.com> Co-authored-by: github-actions <github-actions@github.com> Co-authored-by: Alexander Brown <drjkl@comfy.org>
122 lines
3.8 KiB
Vue
122 lines
3.8 KiB
Vue
<script setup lang="ts">
|
|
import { ref } from 'vue'
|
|
|
|
import type { Locale } from '../../i18n/translations'
|
|
|
|
import {
|
|
getTutorialPosterSrc,
|
|
learningTutorials
|
|
} from '../../data/learningTutorials'
|
|
import { t } from '../../i18n/translations'
|
|
import Badge from '../ui/badge/Badge.vue'
|
|
import { ButtonMask } from '../ui/button-mask'
|
|
import TutorialDetailDialog from './TutorialDetailDialog.vue'
|
|
|
|
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
|
|
|
const activeTutorialId = ref<string | null>(null)
|
|
const activeTutorial = () =>
|
|
learningTutorials.find((tutorial) => tutorial.id === activeTutorialId.value)
|
|
</script>
|
|
|
|
<template>
|
|
<section class="max-w-9xl mx-auto px-6 py-16 lg:py-24">
|
|
<h2
|
|
class="mb-12 text-4xl font-light tracking-tight text-primary-comfy-canvas lg:mb-16 lg:text-6xl"
|
|
>
|
|
{{ t('learning.tutorials.heading', locale) }}
|
|
</h2>
|
|
|
|
<ul
|
|
class="grid grid-cols-1 gap-x-6 gap-y-10 md:grid-cols-2 lg:grid-cols-3 lg:gap-x-8"
|
|
>
|
|
<li
|
|
v-for="tutorial in learningTutorials"
|
|
:key="tutorial.id"
|
|
class="bg-transparency-white-t4 flex flex-col gap-4 overflow-hidden rounded-3xl border-0 p-2"
|
|
>
|
|
<button
|
|
type="button"
|
|
class="group relative block aspect-video cursor-pointer overflow-hidden rounded-3xl"
|
|
:aria-label="`${t('learning.tutorials.titlePrefix', locale)} ${tutorial.title[locale]}`"
|
|
@click="activeTutorialId = tutorial.id"
|
|
>
|
|
<video
|
|
:src="getTutorialPosterSrc(tutorial)"
|
|
:poster="tutorial.poster"
|
|
class="size-full object-cover"
|
|
preload="metadata"
|
|
playsinline
|
|
muted
|
|
></video>
|
|
<span
|
|
class="absolute inset-0 flex items-center justify-center"
|
|
aria-hidden="true"
|
|
>
|
|
<span
|
|
class="flex size-14 items-center justify-center rounded-full bg-white/25 backdrop-blur-sm transition-transform group-hover:scale-105 lg:size-16"
|
|
>
|
|
<svg
|
|
class="ml-1 size-5 text-white lg:size-6"
|
|
viewBox="0 0 24 24"
|
|
fill="currentColor"
|
|
aria-hidden="true"
|
|
>
|
|
<path d="M8 5v14l11-7z" />
|
|
</svg>
|
|
</span>
|
|
</span>
|
|
</button>
|
|
|
|
<div class="flex flex-col space-y-3 p-4">
|
|
<div class="flex items-center justify-between gap-4">
|
|
<h3
|
|
class="text-sm/snug text-primary-comfy-canvas lg:text-base/snug"
|
|
>
|
|
{{ t('learning.tutorials.titlePrefix', locale) }}<br />
|
|
{{ tutorial.title[locale] }}
|
|
</h3>
|
|
<ButtonMask
|
|
v-if="tutorial.href"
|
|
as="a"
|
|
:href="tutorial.href"
|
|
icon-position="right"
|
|
class="shrink-0"
|
|
variant="ghost"
|
|
size="default"
|
|
>
|
|
{{ t('cta.tryWorkflow', locale) }}
|
|
<template #icon>
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="3"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
class="size-4"
|
|
>
|
|
<polyline points="9 6 15 12 9 18" />
|
|
</svg>
|
|
</template>
|
|
</ButtonMask>
|
|
</div>
|
|
|
|
<ul class="flex flex-wrap gap-2">
|
|
<li v-for="tag in tutorial.tags" :key="tag">
|
|
<Badge>{{ t(tag, locale) }}</Badge>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
|
|
<TutorialDetailDialog
|
|
v-if="activeTutorial()"
|
|
:tutorial="activeTutorial()!"
|
|
:locale="locale"
|
|
@close="activeTutorialId = null"
|
|
/>
|
|
</section>
|
|
</template>
|