Initial commit

This commit is contained in:
fawney19
2025-12-10 20:52:44 +08:00
commit f784106826
485 changed files with 110993 additions and 0 deletions

View File

@@ -0,0 +1,167 @@
<template>
<section
ref="sectionRef"
class="min-h-screen snap-start flex items-center px-16 lg:px-20 py-20"
>
<div class="max-w-7xl mx-auto grid md:grid-cols-2 gap-12 items-center">
<!-- Content column -->
<div :class="contentOrder">
<!-- Badge -->
<div
class="inline-flex items-center gap-2 rounded-full px-3 py-1 text-xs font-medium mb-4 transition-all duration-500"
:class="badgeClass"
:style="badgeStyle"
>
<component :is="badgeIcon" class="h-3 w-3" />
{{ badgeText }}
</div>
<!-- Title -->
<h2
class="text-4xl md:text-5xl font-bold text-[#191919] dark:text-white mb-6 transition-all duration-700"
:style="titleStyle"
>
{{ title }}
</h2>
<!-- Description -->
<p
class="text-lg text-[#666663] dark:text-gray-300 mb-4 transition-all duration-700"
:style="descStyle"
>
{{ description }}
</p>
<!-- Install command -->
<div
class="mb-4 transition-all duration-700 relative z-10"
:style="cardStyleFn(0)"
>
<div :class="[panelClasses.commandPanel, 'flex flex-wrap items-center gap-3 px-4 py-3']">
<PlatformSelect
:model-value="platformValue"
@update:model-value="$emit('update:platformValue', $event)"
:options="platformOptions"
class="shrink-0"
/>
<div class="flex-1 min-w-[180px]">
<CodeHighlight :code="installCommand" language="bash" dense />
</div>
<button
@click="$emit('copy', installCommand)"
:class="panelClasses.iconButtonSmall"
title="复制配置"
>
<Copy class="h-3.5 w-3.5" />
</button>
</div>
</div>
<!-- Config files -->
<div
v-for="(config, idx) in configFiles"
:key="config.path"
class="transition-all duration-700"
:class="idx < configFiles.length - 1 ? 'mb-3' : ''"
:style="cardStyleFn(idx + 1)"
>
<div :class="[panelClasses.configPanel, 'overflow-hidden']">
<div :class="panelClasses.panelHeader">
<div class="flex items-center justify-between">
<span class="text-xs font-medium text-[#666663] dark:text-muted-foreground">
{{ config.path }}
</span>
<button
@click="$emit('copy', config.content)"
:class="panelClasses.iconButtonSmall"
title="复制配置"
>
<Copy class="h-3.5 w-3.5" />
</button>
</div>
</div>
<div :class="panelClasses.codeBody">
<div class="config-code-wrapper">
<CodeHighlight :code="config.content" :language="config.language" />
</div>
</div>
</div>
</div>
</div>
<!-- Logo placeholder column -->
<div :class="logoOrder" class="flex items-center justify-center h-full min-h-[300px] relative">
<slot name="logo" />
</div>
</div>
</section>
</template>
<script setup lang="ts">
import { ref, computed, type CSSProperties, type Component } from 'vue'
import { Copy } from 'lucide-vue-next'
import PlatformSelect from '@/components/PlatformSelect.vue'
import CodeHighlight from '@/components/CodeHighlight.vue'
import { panelClasses, type PlatformOption } from './home-config'
// Expose section element for parent scroll tracking
const sectionRef = ref<HTMLElement | null>(null)
defineExpose({ sectionEl: sectionRef })
interface ConfigFile {
path: string
content: string
language: string
}
interface Props {
title: string
description: string
badgeIcon: Component
badgeText: string
badgeClass: string
platformValue: string
platformOptions: PlatformOption[]
installCommand: string
configFiles: ConfigFile[]
// Style props
badgeStyle: CSSProperties
titleStyle: CSSProperties
descStyle: CSSProperties
cardStyleFn: (cardIndex: number) => CSSProperties
// Layout: 'left' means content on left, 'right' means content on right
contentPosition?: 'left' | 'right'
}
const props = withDefaults(defineProps<Props>(), {
contentPosition: 'left'
})
defineEmits<{
copy: [text: string]
'update:platformValue': [value: string]
}>()
const contentOrder = computed(() =>
props.contentPosition === 'right' ? 'md:order-2' : ''
)
const logoOrder = computed(() =>
props.contentPosition === 'right' ? 'md:order-1' : ''
)
</script>
<style scoped>
.config-code-wrapper :deep(.code-highlight pre) {
border: none;
border-radius: 0;
margin: 0;
background-color: transparent !important;
padding: 1rem 1.2rem !important;
}
/* Header separator line */
.panel-header {
border-bottom: 1px solid var(--color-border);
}
</style>

View File

@@ -0,0 +1,752 @@
<template>
<div
ref="scrollContainer"
class="relative h-screen overflow-y-auto snap-y snap-mandatory scroll-smooth literary-grid literary-paper"
>
<!-- Fixed scroll indicator -->
<nav class="scroll-indicator">
<button
v-for="(section, index) in sections"
:key="index"
@click="scrollToSection(index)"
class="scroll-indicator-btn group"
>
<span class="scroll-indicator-label">{{ section.name }}</span>
<div
class="scroll-indicator-dot"
:class="{ active: currentSection === index }"
/>
</button>
</nav>
<!-- Header -->
<header class="fixed top-0 left-0 right-0 z-50 border-b border-[#cc785c]/10 dark:border-[rgba(227,224,211,0.12)] bg-[#fafaf7]/90 dark:bg-[#191714]/95 backdrop-blur-xl transition-all">
<div class="mx-auto max-w-7xl px-6 py-4">
<div class="flex items-center justify-between">
<!-- Logo & Brand -->
<div
class="flex items-center gap-3 group/logo cursor-pointer"
@click="scrollToSection(0)"
>
<HeaderLogo size="h-9 w-9" className="text-[#191919] dark:text-white" />
<div class="flex flex-col justify-center">
<h1 class="text-lg font-bold text-[#191919] dark:text-white leading-none">Aether</h1>
<span class="text-[10px] text-[#91918d] dark:text-muted-foreground leading-none mt-1.5 font-medium tracking-wide">API Gateway</span>
</div>
</div>
<!-- Center Navigation -->
<nav class="hidden md:flex items-center gap-2">
<button
v-for="(section, index) in sections"
:key="index"
@click="scrollToSection(index)"
class="group relative px-3 py-2 text-sm font-medium transition"
:class="currentSection === index
? 'text-[#cc785c] dark:text-[#d4a27f]'
: 'text-[#666663] dark:text-muted-foreground hover:text-[#191919] dark:hover:text-white'"
>
{{ section.name }}
<div
class="absolute bottom-0 left-0 right-0 h-0.5 rounded-full transition-all duration-300"
:class="currentSection === index ? 'bg-[#cc785c] dark:bg-[#d4a27f] scale-x-100' : 'bg-transparent scale-x-0'"
/>
</button>
</nav>
<!-- Right Actions -->
<div class="flex items-center gap-3">
<button
@click="toggleDarkMode"
class="flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted/50 transition"
:title="themeMode === 'system' ? '跟随系统' : themeMode === 'dark' ? '深色模式' : '浅色模式'"
>
<SunMoon v-if="themeMode === 'system'" class="h-4 w-4" />
<Sun v-else-if="themeMode === 'light'" class="h-4 w-4" />
<Moon v-else class="h-4 w-4" />
</button>
<RouterLink
v-if="authStore.isAuthenticated"
:to="dashboardPath"
class="rounded-xl bg-[#191919] dark:bg-[#cc785c] px-4 py-2 text-sm font-medium text-white shadow-sm transition hover:bg-[#262625] dark:hover:bg-[#b86d52]"
>
控制台
</RouterLink>
<button
v-else
@click="showLoginDialog = true"
class="rounded-xl bg-[#cc785c] px-4 py-2 text-sm font-medium text-white shadow-lg shadow-[#cc785c]/30 transition hover:bg-[#d4a27f]"
>
登录
</button>
</div>
</div>
</div>
</header>
<!-- Main Content -->
<main class="relative z-10">
<!-- Fixed Logo Container -->
<div class="fixed inset-0 z-20 pointer-events-none flex items-center justify-center overflow-hidden">
<div
class="transform-gpu logo-container"
:class="[currentSection === SECTIONS.HOME ? 'home-section' : '', `logo-transition-${scrollDirection}`]"
:style="fixedLogoStyle"
>
<Transition :name="logoTransitionName">
<AetherLineByLineLogo
v-if="currentSection === SECTIONS.HOME"
ref="aetherLogoRef"
key="aether-logo"
:size="400"
:line-delay="50"
:stroke-duration="1200"
:fill-duration="1500"
:auto-start="false"
:loop="true"
:loop-pause="800"
:stroke-width="3.5"
:cycle-colors="true"
:is-dark="isDark"
/>
<div
v-else
:key="`ripple-wrapper-${currentLogoType}`"
:class="{ 'heartbeat-wrapper': currentSection === SECTIONS.GEMINI && geminiFillComplete }"
>
<RippleLogo
ref="rippleLogoRef"
:type="currentLogoType"
:size="320"
:use-adaptive="false"
:disable-ripple="currentSection === SECTIONS.GEMINI || currentSection === SECTIONS.FEATURES"
:anim-delay="logoTransitionDelay"
:static="currentSection === SECTIONS.FEATURES"
:class="[currentLogoClass, 'logo-active']"
/>
</div>
</Transition>
</div>
</div>
<!-- Section 0: Introduction -->
<section ref="section0" class="min-h-screen snap-start flex items-center justify-center px-16 lg:px-20 py-20">
<div class="max-w-4xl mx-auto text-center">
<div class="h-80 w-full mb-16" />
<h1
class="mb-6 text-5xl md:text-7xl font-bold text-[#191919] dark:text-white leading-tight transition-all duration-700"
:style="getTitleStyle(SECTIONS.HOME)"
>
欢迎使用 <span class="text-primary">Aether</span>
</h1>
<p
class="mb-8 text-xl text-[#666663] dark:text-gray-300 max-w-2xl mx-auto transition-all duration-700"
:style="getDescStyle(SECTIONS.HOME)"
>
AI 开发工具统一接入平台<br />
整合 Claude CodeCodex CLIGemini CLI 等多个 AI 编程助手
</p>
<button
@click="scrollToSection(SECTIONS.CLAUDE)"
class="mt-16 transition-all duration-700 cursor-pointer hover:scale-110"
:style="getScrollIndicatorStyle(SECTIONS.HOME)"
>
<ChevronDown class="h-8 w-8 mx-auto text-[#91918d] dark:text-muted-foreground/80 animate-bounce" />
</button>
</div>
</section>
<!-- Section 1: Claude Code -->
<CliSection
ref="section1"
title="Claude Code"
description="直接在您的终端中释放Claude的原始力量。瞬间搜索百万行代码库。将数小时的流程转化为单一命令。您的工具。您的流程。您的代码库,以思维速度进化。"
:badge-icon="Code2"
badge-text="IDE 集成"
badge-class="bg-[#cc785c]/10 dark:bg-amber-900/30 border border-[#cc785c]/20 dark:border-amber-800 text-[#cc785c] dark:text-amber-400"
v-model:platform-value="claudePlatform"
:platform-options="platformPresets.claude.options"
:install-command="claudeInstallCommand"
:config-files="[{ path: '~/.claude/settings.json', content: claudeConfig, language: 'json' }]"
:badge-style="getBadgeStyle(SECTIONS.CLAUDE)"
:title-style="getTitleStyle(SECTIONS.CLAUDE)"
:desc-style="getDescStyle(SECTIONS.CLAUDE)"
:card-style-fn="(idx) => getCardStyle(SECTIONS.CLAUDE, idx)"
content-position="right"
@copy="copyToClipboard"
/>
<!-- Section 2: Codex CLI -->
<CliSection
ref="section2"
title="Codex CLI"
description="Codex CLI 是一款可在本地终端运行的编程助手工具它能够读取修改并执行用户指定目录中的代码"
:badge-icon="Terminal"
badge-text="命令行工具"
badge-class="bg-[#cc785c]/10 dark:bg-emerald-900/30 border border-[#cc785c]/20 dark:border-emerald-800 text-[#cc785c] dark:text-emerald-400"
v-model:platform-value="codexPlatform"
:platform-options="platformPresets.codex.options"
:install-command="codexInstallCommand"
:config-files="[
{ path: '~/.codex/config.toml', content: codexConfig, language: 'toml' },
{ path: '~/.codex/auth.json', content: codexAuthConfig, language: 'json' }
]"
:badge-style="getBadgeStyle(SECTIONS.CODEX)"
:title-style="getTitleStyle(SECTIONS.CODEX)"
:desc-style="getDescStyle(SECTIONS.CODEX)"
:card-style-fn="(idx) => getCardStyle(SECTIONS.CODEX, idx)"
content-position="left"
@copy="copyToClipboard"
/>
<!-- Section 3: Gemini CLI -->
<CliSection
ref="section3"
title="Gemini CLI"
description="Gemini CLI 是一款开源人工智能代理可将 Gemini 的强大功能直接带入你的终端它提供了对 Gemini 的轻量级访问为你提供了从提示符到我们模型的最直接路径"
:badge-icon="Sparkles"
badge-text="多模态 AI"
badge-class="bg-[#cc785c]/10 dark:bg-primary/20 border border-[#cc785c]/20 dark:border-primary/30 text-[#cc785c] dark:text-primary"
v-model:platform-value="geminiPlatform"
:platform-options="platformPresets.gemini.options"
:install-command="geminiInstallCommand"
:config-files="[
{ path: '~/.gemini/.env', content: geminiEnvConfig, language: 'dotenv' },
{ path: '~/.gemini/settings.json', content: geminiSettingsConfig, language: 'json' }
]"
:badge-style="getBadgeStyle(SECTIONS.GEMINI)"
:title-style="getTitleStyle(SECTIONS.GEMINI)"
:desc-style="getDescStyle(SECTIONS.GEMINI)"
:card-style-fn="(idx) => getCardStyle(SECTIONS.GEMINI, idx)"
content-position="right"
@copy="copyToClipboard"
>
<template #logo>
<GeminiStarCluster :is-visible="currentSection === SECTIONS.GEMINI && sectionVisibility[SECTIONS.GEMINI] > 0.05" />
</template>
</CliSection>
<!-- Section 4: Features -->
<section ref="section4" class="min-h-screen snap-start flex items-center justify-center px-16 lg:px-20 py-20 relative overflow-hidden">
<div class="max-w-4xl mx-auto text-center relative z-10">
<div
class="inline-flex items-center gap-2 rounded-full bg-[#cc785c]/10 dark:bg-purple-500/20 border border-[#cc785c]/20 dark:border-purple-500/40 px-4 py-2 text-sm font-medium text-[#cc785c] dark:text-purple-300 mb-6 backdrop-blur-sm transition-all duration-500"
:style="getBadgeStyle(SECTIONS.FEATURES)"
>
<Sparkles class="h-4 w-4" />
项目进度
</div>
<h2
class="text-4xl md:text-5xl font-bold text-[#191919] dark:text-white mb-6 transition-all duration-700"
:style="getTitleStyle(SECTIONS.FEATURES)"
>
功能开发进度
</h2>
<p
class="text-lg text-[#666663] dark:text-gray-300 mb-12 max-w-2xl mx-auto transition-all duration-700"
:style="getDescStyle(SECTIONS.FEATURES)"
>
核心 API 代理功能已完成,正在载入更多功能
</p>
<div class="grid md:grid-cols-3 gap-6">
<div
v-for="(feature, idx) in featureCards"
:key="idx"
class="bg-white/70 dark:bg-[#262624]/80 backdrop-blur-sm rounded-2xl p-6 border border-[#e5e4df] dark:border-[rgba(227,224,211,0.16)] hover:border-[#cc785c]/30 dark:hover:border-[#d4a27f]/40 transition-all duration-700"
:style="getFeatureCardStyle(SECTIONS.FEATURES, idx)"
>
<div
class="flex h-12 w-12 items-center justify-center rounded-xl mb-4 mx-auto"
:class="feature.status === 'completed'
? 'bg-emerald-500/10 dark:bg-emerald-500/15'
: 'bg-[#cc785c]/10 dark:bg-[#cc785c]/15'"
>
<component
:is="feature.icon"
class="h-6 w-6"
:class="feature.status === 'completed'
? 'text-emerald-500 dark:text-emerald-400'
: 'text-[#cc785c] dark:text-[#d4a27f] animate-spin'"
/>
</div>
<h3 class="text-lg font-bold text-[#191919] dark:text-white mb-2">{{ feature.title }}</h3>
<p class="text-sm text-[#666663] dark:text-[#c9c3b4]">{{ feature.desc }}</p>
<div
class="mt-3 inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium"
:class="feature.status === 'completed'
? 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
: 'bg-amber-500/10 text-amber-600 dark:text-amber-400'"
>
{{ feature.status === 'completed' ? '已完成' : '进行中' }}
</div>
</div>
</div>
<div class="mt-12 transition-all duration-700" :style="getButtonsStyle(SECTIONS.FEATURES)">
<RouterLink
v-if="authStore.isAuthenticated"
:to="dashboardPath"
class="inline-flex items-center gap-2 rounded-xl bg-primary hover:bg-primary/90 px-6 py-3 text-base font-semibold text-white shadow-lg shadow-primary/30 transition hover:shadow-primary/50 hover:scale-105"
>
<Rocket class="h-5 w-5" />
立即开始使用
</RouterLink>
<button
v-else
@click="showLoginDialog = true"
class="inline-flex items-center gap-2 rounded-xl bg-primary hover:bg-primary/90 px-6 py-3 text-base font-semibold text-white shadow-lg shadow-primary/30 transition hover:shadow-primary/50 hover:scale-105"
>
<Rocket class="h-5 w-5" />
立即开始使用
</button>
</div>
</div>
</section>
</main>
<!-- Footer -->
<footer class="relative z-10 border-t border-[#cc785c]/10 dark:border-[rgba(227,224,211,0.12)] bg-[#fafaf7]/90 dark:bg-[#191714]/95 backdrop-blur-md py-8">
<div class="mx-auto max-w-7xl px-6">
<div class="flex flex-col items-center justify-between gap-4 sm:flex-row">
<p class="text-sm text-[#91918d] dark:text-muted-foreground">© 2025 Aether. 团队内部使用</p>
<div class="flex items-center gap-6 text-sm text-[#91918d] dark:text-muted-foreground">
<a href="#" class="transition hover:text-[#191919] dark:hover:text-white">使用条款</a>
<a href="#" class="transition hover:text-[#191919] dark:hover:text-white">隐私政策</a>
<a href="#" class="transition hover:text-[#191919] dark:hover:text-white">技术支持</a>
</div>
</div>
</div>
</footer>
<LoginDialog v-model="showLoginDialog" />
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { RouterLink } from 'vue-router'
import {
ChevronDown,
Code2,
Moon,
Rocket,
Sparkles,
Sun,
SunMoon,
Terminal
} from 'lucide-vue-next'
import { useAuthStore } from '@/stores/auth'
import { useDarkMode } from '@/composables/useDarkMode'
import { useClipboard } from '@/composables/useClipboard'
import LoginDialog from '@/features/auth/components/LoginDialog.vue'
import RippleLogo from '@/components/RippleLogo.vue'
import HeaderLogo from '@/components/HeaderLogo.vue'
import AetherLineByLineLogo from '@/components/AetherLineByLineLogo.vue'
import GeminiStarCluster from '@/components/GeminiStarCluster.vue'
import CliSection from './CliSection.vue'
import {
SECTIONS,
sections,
featureCards,
useCliConfigs,
platformPresets,
getInstallCommand,
getLogoType,
getLogoClass
} from './home-config'
import {
useSectionAnimations,
useLogoPosition,
useLogoTransition
} from './useSectionAnimations'
const authStore = useAuthStore()
const { isDark, themeMode, toggleDarkMode } = useDarkMode()
const { copyToClipboard } = useClipboard()
const dashboardPath = computed(() =>
authStore.user?.role === 'admin' ? '/admin/dashboard' : '/dashboard'
)
const baseUrl = computed(() => window.location.origin)
// Scroll state
const scrollContainer = ref<HTMLElement | null>(null)
const currentSection = ref(0)
const previousSection = ref(0)
const scrollDirection = ref<'up' | 'down'>('down')
const windowWidth = ref(typeof window !== 'undefined' ? window.innerWidth : 1024)
const sectionVisibility = ref<number[]>([0, 0, 0, 0, 0])
let lastScrollY = 0
// Section refs - section0 and section4 are direct HTML elements, section1-3 are CliSection components
const section0 = ref<HTMLElement | null>(null)
const section1 = ref<InstanceType<typeof CliSection> | null>(null)
const section2 = ref<InstanceType<typeof CliSection> | null>(null)
const section3 = ref<InstanceType<typeof CliSection> | null>(null)
const section4 = ref<HTMLElement | null>(null)
// Helper to get DOM element from ref (handles both direct elements and component instances)
const getSectionElement = (index: number): HTMLElement | null => {
switch (index) {
case 0: return section0.value
case 1: return (section1.value?.sectionEl as HTMLElement | null | undefined) ?? null
case 2: return (section2.value?.sectionEl as HTMLElement | null | undefined) ?? null
case 3: return (section3.value?.sectionEl as HTMLElement | null | undefined) ?? null
case 4: return section4.value
default: return null
}
}
// Logo refs
const aetherLogoRef = ref<InstanceType<typeof AetherLineByLineLogo> | null>(null)
const rippleLogoRef = ref<InstanceType<typeof RippleLogo> | null>(null)
const hasLogoAnimationStarted = ref(false)
const geminiFillComplete = ref(false)
// Animation composables
const {
getBadgeStyle,
getTitleStyle,
getDescStyle,
getButtonsStyle,
getScrollIndicatorStyle,
getCardStyle,
getFeatureCardStyle
} = useSectionAnimations(sectionVisibility)
const { fixedLogoStyle } = useLogoPosition(currentSection, windowWidth)
const { logoTransitionName } = useLogoTransition(currentSection, previousSection)
// Logo computed
const currentLogoType = computed(() => getLogoType(currentSection.value))
const currentLogoClass = computed(() => getLogoClass(currentSection.value))
const logoTransitionDelay = computed(() => {
if (currentSection.value === SECTIONS.FEATURES) return 0
if (previousSection.value === SECTIONS.FEATURES) return 200
return 500
})
// Platform states
const claudePlatform = ref(platformPresets.claude.defaultValue)
const codexPlatform = ref(platformPresets.codex.defaultValue)
const geminiPlatform = ref(platformPresets.gemini.defaultValue)
// Install commands
const claudeInstallCommand = computed(() => getInstallCommand('claude', claudePlatform.value))
const codexInstallCommand = computed(() => getInstallCommand('codex', codexPlatform.value))
const geminiInstallCommand = computed(() => getInstallCommand('gemini', geminiPlatform.value))
// CLI configs
const { claudeConfig, codexConfig, codexAuthConfig, geminiEnvConfig, geminiSettingsConfig } =
useCliConfigs(baseUrl)
// Dialog state
const showLoginDialog = ref(false)
// Scroll handling
let scrollEndTimer: ReturnType<typeof setTimeout> | null = null
const calculateVisibility = (element: HTMLElement | null): number => {
if (!element) return 0
const rect = element.getBoundingClientRect()
const containerHeight = window.innerHeight
if (rect.bottom < 0 || rect.top > containerHeight) return 0
const elementCenter = rect.top + rect.height / 2
const viewportCenter = containerHeight / 2
const distanceFromCenter = Math.abs(elementCenter - viewportCenter)
const maxDistance = containerHeight / 2 + rect.height / 2
return Math.max(0, 1 - distanceFromCenter / maxDistance)
}
const handleScroll = () => {
if (!scrollContainer.value) return
const containerHeight = window.innerHeight
const newScrollY = scrollContainer.value.scrollTop
// Track scroll direction
scrollDirection.value = newScrollY > lastScrollY ? 'down' : 'up'
lastScrollY = newScrollY
// Update visibility
for (let i = 0; i < 5; i++) {
sectionVisibility.value[i] = calculateVisibility(getSectionElement(i))
}
// Update current section
const scrollMiddle = newScrollY + containerHeight / 2
for (let i = 4; i >= 0; i--) {
const section = getSectionElement(i)
if (section && section.offsetTop <= scrollMiddle) {
if (currentSection.value !== i) {
previousSection.value = currentSection.value
currentSection.value = i
hasLogoAnimationStarted.value = false
}
break
}
}
// Detect snap complete
if (scrollEndTimer) clearTimeout(scrollEndTimer)
scrollEndTimer = setTimeout(() => {
if (currentSection.value === SECTIONS.HOME && !hasLogoAnimationStarted.value) {
hasLogoAnimationStarted.value = true
setTimeout(() => aetherLogoRef.value?.startAnimation(), 100)
}
}, 150)
}
const scrollToSection = (index: number) => {
const target = getSectionElement(index)
if (target) target.scrollIntoView({ behavior: 'smooth' })
}
// Watch Gemini fill complete
watch(
() => rippleLogoRef.value?.fillComplete,
(val) => {
if (currentSection.value === SECTIONS.GEMINI && val) geminiFillComplete.value = true
}
)
watch(currentSection, (_, old) => {
if (old === SECTIONS.GEMINI) geminiFillComplete.value = false
})
const handleResize = () => {
windowWidth.value = window.innerWidth
}
onMounted(() => {
scrollContainer.value?.addEventListener('scroll', handleScroll, { passive: true })
window.addEventListener('resize', handleResize, { passive: true })
handleScroll()
// Initial animation
setTimeout(() => {
if (currentSection.value === SECTIONS.HOME && !hasLogoAnimationStarted.value) {
hasLogoAnimationStarted.value = true
setTimeout(() => aetherLogoRef.value?.startAnimation(), 100)
}
}, 300)
})
onUnmounted(() => {
scrollContainer.value?.removeEventListener('scroll', handleScroll)
window.removeEventListener('resize', handleResize)
if (scrollEndTimer) clearTimeout(scrollEndTimer)
})
</script>
<style scoped>
/* Typography */
h1, h2, h3 {
font-family: var(--serif);
letter-spacing: -0.02em;
font-weight: 500;
}
p {
font-family: var(--serif);
letter-spacing: 0.01em;
line-height: 1.7;
}
button, nav, a, .inline-flex {
font-family: var(--sans-serif);
}
/* Panel styles */
.command-panel-surface {
border-color: var(--color-border);
background: rgba(255, 255, 255, 0.5);
backdrop-filter: blur(12px);
}
.dark .command-panel-surface {
background: rgba(38, 38, 36, 0.3);
}
/* Performance */
h1, h2, p {
will-change: transform, opacity;
}
/* Scroll indicator */
.scroll-indicator {
position: fixed;
right: 2rem;
top: 50%;
transform: translateY(-50%);
z-index: 9999;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
@media (max-width: 1023px) {
.scroll-indicator {
display: none;
}
}
.scroll-indicator-btn {
position: relative;
display: flex;
align-items: center;
justify-content: flex-end;
padding: 0.25rem;
}
.scroll-indicator-label {
position: absolute;
right: 1.5rem;
font-size: 0.75rem;
font-weight: 500;
color: #666663;
opacity: 0;
transition: opacity 0.2s ease;
white-space: nowrap;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(8px);
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
pointer-events: none;
}
.dark .scroll-indicator-label {
color: #a0a0a0;
background: rgba(25, 23, 20, 0.9);
}
.scroll-indicator-btn:hover .scroll-indicator-label {
opacity: 1;
}
.scroll-indicator-dot {
width: 10px;
height: 10px;
border-radius: 50%;
border: 2px solid #d4d4d4;
background: transparent;
transition: all 0.3s ease;
}
.dark .scroll-indicator-dot {
border-color: #4a4a4a;
}
.scroll-indicator-dot.active {
background: #cc785c;
border-color: #cc785c;
transform: scale(1.3);
}
/* Logo transitions */
.logo-scale-enter-active {
transition: opacity 0.5s ease-out, transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.logo-scale-leave-active {
transition: opacity 0.3s ease-in, transform 0.3s ease-in;
}
.logo-scale-enter-from {
opacity: 0;
transform: scale(0.6) rotate(-8deg);
}
.logo-scale-leave-to {
opacity: 0;
transform: scale(1.2) rotate(8deg);
}
.logo-slide-left-enter-active,
.logo-slide-right-enter-active {
transition: opacity 0.4s ease-out, transform 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.logo-slide-left-leave-active,
.logo-slide-right-leave-active {
transition: opacity 0.25s ease-in, transform 0.3s ease-in;
}
.logo-slide-left-enter-from {
opacity: 0;
transform: translateX(60px) scale(0.9);
}
.logo-slide-left-leave-to {
opacity: 0;
transform: translateX(-60px) scale(0.9);
}
.logo-slide-right-enter-from {
opacity: 0;
transform: translateX(-60px) scale(0.9);
}
.logo-slide-right-leave-to {
opacity: 0;
transform: translateX(60px) scale(0.9);
}
/* Logo container */
.logo-container {
width: 320px;
height: 320px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.logo-container.home-section {
width: 400px;
height: 400px;
}
.logo-container > * {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
@media (max-width: 768px) {
.logo-container {
width: 240px;
height: 240px;
}
.logo-container.home-section {
width: 280px;
height: 280px;
}
}
/* Heartbeat animation */
.heartbeat-wrapper {
animation: heartbeat 1.5s ease-in-out infinite;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
@keyframes heartbeat {
0%, 70%, 100% { transform: scale(1); }
14% { transform: scale(1.06); }
28% { transform: scale(1); }
42% { transform: scale(1.1); }
}
</style>

View File

@@ -0,0 +1,253 @@
<template>
<div class="min-h-screen bg-[#fafaf7] dark:bg-[#191714] p-8">
<div class="max-w-7xl mx-auto">
<h1 class="text-3xl font-bold text-center mb-2 text-[#191919] dark:text-white">Logo 颜色方案对比</h1>
<p class="text-center text-[#666663] dark:text-gray-400 mb-8">点击任意方案可以放大预览</p>
<!-- Color schemes grid -->
<div class="grid grid-cols-2 md:grid-cols-3 gap-6">
<div
v-for="(scheme, index) in colorSchemes"
:key="index"
class="relative bg-white dark:bg-[#262624] rounded-2xl p-6 border border-[#e5e4df] dark:border-[rgba(227,224,211,0.16)] cursor-pointer transition-all hover:shadow-lg hover:scale-[1.02]"
:class="{ 'ring-2 ring-primary': selectedScheme === index }"
@click="selectScheme(index)"
>
<!-- Scheme name badge -->
<div class="absolute top-3 left-3 px-2 py-1 rounded-full text-xs font-medium"
:style="{ backgroundColor: scheme.primary + '20', color: scheme.primary }">
{{ scheme.name }}
</div>
<!-- Logo preview -->
<div class="flex items-center justify-center h-48 mb-4">
<svg
viewBox="0 0 800 800"
class="w-40 h-40"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<linearGradient :id="`gradient-${index}`" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" :stop-color="scheme.primary" />
<stop offset="50%" :stop-color="scheme.secondary" />
<stop offset="100%" :stop-color="scheme.primary" />
</linearGradient>
</defs>
<!-- Fill -->
<path
:d="fullPath"
:fill="`url(#gradient-${index})`"
fill-rule="evenodd"
opacity="0.7"
/>
<!-- Lines -->
<path
v-for="(path, pathIndex) in linePaths"
:key="pathIndex"
:d="path"
fill="none"
:stroke="scheme.primary"
stroke-width="3.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</div>
<!-- Color swatches -->
<div class="flex items-center justify-center gap-3">
<div class="flex flex-col items-center">
<div
class="w-8 h-8 rounded-full border-2 border-white shadow"
:style="{ backgroundColor: scheme.primary }"
></div>
<span class="text-xs text-[#666663] dark:text-gray-400 mt-1">{{ scheme.primary }}</span>
</div>
<div class="flex flex-col items-center">
<div
class="w-8 h-8 rounded-full border-2 border-white shadow"
:style="{ backgroundColor: scheme.secondary }"
></div>
<span class="text-xs text-[#666663] dark:text-gray-400 mt-1">{{ scheme.secondary }}</span>
</div>
</div>
<!-- Description -->
<p class="text-center text-sm text-[#666663] dark:text-gray-400 mt-3">{{ scheme.description }}</p>
</div>
</div>
<!-- Large preview modal -->
<Teleport to="body">
<div
v-if="showPreview"
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"
@click="showPreview = false"
>
<div
class="bg-white dark:bg-[#262624] rounded-3xl p-8 max-w-lg w-full mx-4 shadow-2xl"
@click.stop
>
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-bold text-[#191919] dark:text-white">
{{ colorSchemes[selectedScheme].name }}
</h2>
<button
@click="showPreview = false"
class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<!-- Animated logo -->
<div class="flex items-center justify-center py-8">
<AetherLineByLineLogo
:key="selectedScheme"
:size="300"
:line-delay="120"
:stroke-duration="2000"
:color-duration="1200"
:auto-start="true"
:loop="true"
:loop-pause="300"
:stroke-width="3.5"
:outline-color="colorSchemes[selectedScheme].primary"
/>
</div>
<!-- Color info -->
<div class="flex items-center justify-center gap-6 mt-4">
<div class="flex items-center gap-2">
<div
class="w-6 h-6 rounded-full border-2 border-white shadow"
:style="{ backgroundColor: colorSchemes[selectedScheme].primary }"
></div>
<span class="text-sm font-mono text-[#666663] dark:text-gray-400">
{{ colorSchemes[selectedScheme].primary }}
</span>
</div>
<div class="flex items-center gap-2">
<div
class="w-6 h-6 rounded-full border-2 border-white shadow"
:style="{ backgroundColor: colorSchemes[selectedScheme].secondary }"
></div>
<span class="text-sm font-mono text-[#666663] dark:text-gray-400">
{{ colorSchemes[selectedScheme].secondary }}
</span>
</div>
</div>
<!-- Apply button -->
<div class="mt-6 text-center">
<button
@click="applyScheme"
class="px-6 py-2 bg-primary text-white rounded-xl font-medium hover:bg-primary/90 transition"
>
应用此方案
</button>
</div>
</div>
</div>
</Teleport>
<!-- Back button -->
<div class="mt-8 text-center">
<RouterLink
to="/"
class="inline-flex items-center gap-2 px-4 py-2 text-[#666663] dark:text-gray-400 hover:text-[#191919] dark:hover:text-white transition"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
返回首页
</RouterLink>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { RouterLink } from 'vue-router'
import AetherLineByLineLogo from '@/components/AetherLineByLineLogo.vue'
import { AETHER_LINE_PATHS, AETHER_FULL_PATH } from '@/constants/logoPaths'
const linePaths = AETHER_LINE_PATHS
const fullPath = AETHER_FULL_PATH
const colorSchemes = [
{
name: '当前配色 - 暖橙',
primary: '#cc785c',
secondary: '#e8a882',
description: '温暖的赤陶色,亲和力强'
},
{
name: '深金色调',
primary: '#b08d57',
secondary: '#d4b896',
description: '古铜金色,更加沉稳大气'
},
{
name: '玫瑰红调',
primary: '#a4636c',
secondary: '#d4a5aa',
description: '优雅的玫瑰红,细腻温婉'
},
{
name: '青铜绿调',
primary: '#7a8c70',
secondary: '#a8b8a0',
description: '自然青铜色,清新科技感'
},
{
name: '深紫藤调',
primary: '#7d6b8a',
secondary: '#b5a8c2',
description: '紫藤色调,神秘优雅'
},
{
name: '黑金商务',
primary: '#3d3833',
secondary: '#8b7355',
description: '炭黑配金色,高端商务风'
},
{
name: '海蓝科技',
primary: '#4a7c8c',
secondary: '#8ab8c8',
description: '深海蓝色,科技专业感'
},
{
name: '焦糖棕调',
primary: '#8b6b4a',
secondary: '#c4a882',
description: '焦糖棕色,复古温暖'
},
{
name: '石墨灰调',
primary: '#5a5a5a',
secondary: '#9a9a9a',
description: '中性石墨色,简约现代'
}
]
const selectedScheme = ref(0)
const showPreview = ref(false)
const selectScheme = (index: number) => {
selectedScheme.value = index
showPreview.value = true
}
const applyScheme = () => {
const scheme = colorSchemes[selectedScheme.value]
alert(`应用方案: ${scheme.name}\n\n请将以下颜色值更新到代码中:\n主色: ${scheme.primary}\n过渡色: ${scheme.secondary}\n\n需要修改的文件:\n1. AetherLineByLineLogo.vue - gradient 颜色\n2. Home.vue - outlineColor 属性`)
showPreview.value = false
}
</script>

View File

@@ -0,0 +1,171 @@
import { computed, type Ref } from 'vue'
import { Apple, Box, Check, Loader2, Monitor, Terminal } from 'lucide-vue-next'
import type { Component } from 'vue'
// Section index constants
export const SECTIONS = {
HOME: 0,
CLAUDE: 1,
CODEX: 2,
GEMINI: 3,
FEATURES: 4
} as const
export type SectionIndex = (typeof SECTIONS)[keyof typeof SECTIONS]
// Section navigation configuration
export const sections = [
{ name: '首页' },
{ name: 'Claude' },
{ name: 'OpenAI' },
{ name: 'Gemini' },
{ name: '更多' }
] as const
// Feature cards data
export const featureCards = [
{
icon: Check,
title: 'Claude / OpenAI / Gemini',
desc: '已完整接入三大主流 AI 编程助手的标准 API',
status: 'completed' as const
},
{
icon: Check,
title: '灵活的 API 扩展',
desc: '插件化架构,可快速接入其他 LLM Provider',
status: 'completed' as const
},
{
icon: Loader2,
title: '项目追踪',
desc: '开发日志、代码 Review、文档生成等功能即将到来',
status: 'in-progress' as const
}
]
// CLI configuration generators
export function useCliConfigs(baseUrl: Ref<string>) {
const claudeConfig = computed(() => `{
"env": {
"ANTHROPIC_AUTH_TOKEN": "your-api-key",
"ANTHROPIC_BASE_URL": "${baseUrl.value}"
}
}`)
const codexConfig = computed(() => `model_provider = "aether"
model = "latest-model-name"
model_reasoning_effort = "high"
network_access = "enabled"
disable_response_storage = true
[model_providers.aether]
name = "aether"
base_url = "${baseUrl.value}/v1"
wire_api = "responses"
requires_openai_auth = true`)
const codexAuthConfig = computed(() => `{
"OPENAI_API_KEY": "your-api-key"
}`)
const geminiEnvConfig = computed(() => `GOOGLE_GEMINI_BASE_URL=${baseUrl.value}
GEMINI_API_KEY=your-api-key
GEMINI_MODEL=latest-model-name`)
const geminiSettingsConfig = computed(() => `{
"ide": {
"enabled": true
},
"security": {
"auth": {
"selectedType": "gemini-api-key"
}
}
}`)
return {
claudeConfig,
codexConfig,
codexAuthConfig,
geminiEnvConfig,
geminiSettingsConfig
}
}
// CSS class constants
export const panelClasses = {
commandPanel: 'rounded-xl border command-panel-surface',
configPanel: 'rounded-xl border config-panel',
panelHeader: 'px-4 py-2 panel-header',
codeBody: 'code-panel-body',
iconButtonSmall: [
'flex items-center justify-center rounded-lg border h-7 w-7',
'border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)]',
'bg-transparent',
'text-[#666663] dark:text-[#f1ead8]',
'transition hover:bg-[#f0f0eb] dark:hover:bg-[#3a3731]'
].join(' ')
} as const
// Platform option type
export interface PlatformOption {
value: string
label: string
hint: string
icon: Component
command: string
}
// Platform presets configuration
export const platformPresets = {
claude: {
options: [
{ value: 'mac', label: 'Mac / Linux', hint: 'Terminal', icon: Terminal, command: 'curl -fsSL https://claude.ai/install.sh | bash' },
{ value: 'windows', label: 'Windows', hint: 'PowerShell', icon: Monitor, command: 'irm https://claude.ai/install.ps1 | iex' },
{ value: 'nodejs', label: 'Node.js', hint: 'npm', icon: Box, command: 'npm install -g @anthropic-ai/claude-code' },
{ value: 'homebrew', label: 'Mac', hint: 'Homebrew', icon: Apple, command: 'brew install --cask claude-code' }
] as PlatformOption[],
defaultValue: 'mac'
},
codex: {
options: [
{ value: 'nodejs', label: 'Node.js', hint: 'npm', icon: Box, command: 'npm install -g @openai/codex' },
{ value: 'homebrew', label: 'Mac', hint: 'Homebrew', icon: Apple, command: 'brew install --cask codex' }
] as PlatformOption[],
defaultValue: 'nodejs'
},
gemini: {
options: [
{ value: 'nodejs', label: 'Node.js', hint: 'npm', icon: Box, command: 'npm install -g @google/gemini-cli' },
{ value: 'homebrew', label: 'Mac', hint: 'Homebrew', icon: Apple, command: 'brew install gemini-cli' }
] as PlatformOption[],
defaultValue: 'nodejs'
}
} as const
// Helper to get command by platform value
export function getInstallCommand(preset: keyof typeof platformPresets, value: string): string {
const config = platformPresets[preset]
return config.options.find((opt) => opt.value === value)?.command ?? ''
}
// Logo type mapping
export function getLogoType(section: number): 'claude' | 'openai' | 'gemini' | 'aether' {
switch (section) {
case SECTIONS.CLAUDE: return 'claude'
case SECTIONS.CODEX: return 'openai'
case SECTIONS.GEMINI: return 'gemini'
default: return 'aether'
}
}
// Logo color class mapping
export function getLogoClass(section: number): string {
switch (section) {
case SECTIONS.CLAUDE: return 'text-[#D97757]'
case SECTIONS.CODEX: return 'text-[#191919] dark:text-white'
case SECTIONS.GEMINI: return '' // Gemini uses gradient
default: return 'text-[#191919] dark:text-white'
}
}

View File

@@ -0,0 +1,199 @@
import { computed, type Ref, type CSSProperties } from 'vue'
import { SECTIONS } from './home-config'
// Animation configuration constants
const ANIMATION_CONFIG = {
delays: {
badge: 0,
title: 0.1,
desc: 0.2,
buttons: 0.3,
scrollIndicator: 0.4,
cardBase: 0.25,
cardIncrement: 0.1,
featureCardBase: 0.2,
featureCardIncrement: 0.15
},
translateY: {
home: 30,
cli: 10,
badge: 8,
featureCard: 30
},
translateX: {
badge: 24,
title: 32,
desc: 28,
buttons: 24,
card: 20
}
} as const
// Get horizontal direction based on section layout
// Claude(1) and Gemini(3) content on right, slide from right
// Codex(2) content on left, slides from left
function getDirectionMultiplier(index: number): number {
if (index === SECTIONS.CLAUDE || index === SECTIONS.GEMINI) return 1
if (index === SECTIONS.CODEX) return -1
return 0
}
function getHorizontalOffset(index: number, distance: number, progress: number): number {
const direction = getDirectionMultiplier(index)
if (direction === 0) return 0
return (1 - progress) * distance * direction
}
export function useSectionAnimations(sectionVisibility: Ref<number[]>) {
const { delays, translateY, translateX } = ANIMATION_CONFIG
// Style generators for different elements
const getBadgeStyle = (index: number): CSSProperties => {
const visibility = sectionVisibility.value[index]
const opacity = Math.min(1, visibility * 3)
const progress = Math.min(1, visibility * 2)
const direction = getDirectionMultiplier(index)
const offsetX = getHorizontalOffset(index, translateX.badge, progress)
const offsetY = direction === 0 ? (1 - progress) * translateY.badge : 0
return {
opacity,
transform: `translate(${offsetX}px, ${offsetY}px)`
}
}
const getTitleStyle = (index: number): CSSProperties => {
const visibility = sectionVisibility.value[index]
const adjustedVisibility = Math.max(0, visibility - delays.title) / (1 - delays.title)
const progress = Math.min(1, adjustedVisibility * 2)
const yBase = getDirectionMultiplier(index) === 0 ? translateY.home : translateY.cli
const offsetY = (1 - progress) * yBase
const offsetX = getHorizontalOffset(index, translateX.title, progress)
return {
opacity: progress,
transform: `translate(${offsetX}px, ${offsetY}px)`
}
}
const getDescStyle = (index: number): CSSProperties => {
const visibility = sectionVisibility.value[index]
const adjustedVisibility = Math.max(0, visibility - delays.desc) / (1 - delays.desc)
const progress = Math.min(1, adjustedVisibility * 2)
const yBase = getDirectionMultiplier(index) === 0 ? translateY.home : translateY.badge
const offsetY = (1 - progress) * yBase
const offsetX = getHorizontalOffset(index, translateX.desc, progress)
return {
opacity: progress,
transform: `translate(${offsetX}px, ${offsetY}px)`
}
}
const getButtonsStyle = (index: number): CSSProperties => {
const visibility = sectionVisibility.value[index]
const adjustedVisibility = Math.max(0, visibility - delays.buttons) / (1 - delays.buttons)
const progress = Math.min(1, adjustedVisibility * 2)
const yBase = getDirectionMultiplier(index) === 0 ? 20 : translateY.badge
const offsetY = (1 - progress) * yBase
const offsetX = getHorizontalOffset(index, translateX.buttons, progress)
return {
opacity: progress,
transform: `translate(${offsetX}px, ${offsetY}px)`
}
}
const getScrollIndicatorStyle = (index: number): CSSProperties => {
const visibility = sectionVisibility.value[index]
const adjustedVisibility = Math.max(0, visibility - delays.scrollIndicator) / (1 - delays.scrollIndicator)
const opacity = Math.min(1, adjustedVisibility * 2)
return { opacity }
}
const getCardStyle = (sectionIndex: number, cardIndex: number): CSSProperties => {
const visibility = sectionVisibility.value[sectionIndex]
const totalDelay = delays.cardBase + cardIndex * delays.cardIncrement
const adjustedVisibility = Math.max(0, visibility - totalDelay) / (1 - totalDelay)
const progress = Math.min(1, adjustedVisibility * 2)
const yBase = getDirectionMultiplier(sectionIndex) === 0 ? 20 : translateY.cli
const offsetY = (1 - progress) * yBase
const offsetX = getHorizontalOffset(sectionIndex, translateX.card, progress)
return {
opacity: progress,
transform: `translate(${offsetX}px, ${offsetY}px)`
}
}
const getFeatureCardStyle = (sectionIndex: number, cardIndex: number): CSSProperties => {
const visibility = sectionVisibility.value[sectionIndex]
const totalDelay = delays.featureCardBase + cardIndex * delays.featureCardIncrement
const adjustedVisibility = Math.max(0, visibility - totalDelay) / (1 - totalDelay)
const opacity = Math.min(1, adjustedVisibility * 2)
const offsetY = (1 - Math.min(1, adjustedVisibility * 2)) * translateY.featureCard
const scale = 0.9 + Math.min(1, adjustedVisibility * 2) * 0.1
return {
opacity,
transform: `translateY(${offsetY}px) scale(${scale})`
}
}
return {
getBadgeStyle,
getTitleStyle,
getDescStyle,
getButtonsStyle,
getScrollIndicatorStyle,
getCardStyle,
getFeatureCardStyle
}
}
// Fixed logo position style based on current section
export function useLogoPosition(
currentSection: Ref<number>,
windowWidth: Ref<number>
) {
const fixedLogoStyle = computed(() => {
const section = currentSection.value
const isDesktop = windowWidth.value >= 768
let transform = ''
let opacity = 1
if (section === SECTIONS.HOME) {
transform = 'scale(1.1) translateY(-18vh)'
opacity = 0.25
} else if (section === SECTIONS.CLAUDE) {
transform = isDesktop ? 'translateX(-25vw) scale(1)' : 'translateY(-20vh) scale(0.8)'
} else if (section === SECTIONS.CODEX) {
transform = isDesktop ? 'translateX(25vw) scale(1)' : 'translateY(-20vh) scale(0.8)'
} else if (section === SECTIONS.GEMINI) {
transform = isDesktop ? 'translateX(-25vw) scale(1)' : 'translateY(-20vh) scale(0.8)'
} else {
transform = isDesktop ? 'translateX(0) scale(1)' : 'translateY(-20vh) scale(0.8)'
opacity = 0.15
}
return {
transform,
opacity,
transition: 'transform 0.8s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.6s ease-out'
}
})
return { fixedLogoStyle }
}
// Logo transition name based on scroll direction
export function useLogoTransition(
currentSection: Ref<number>,
previousSection: Ref<number>
) {
const logoTransitionName = computed(() => {
if (currentSection.value === SECTIONS.HOME || previousSection.value === SECTIONS.HOME) {
return 'logo-scale'
}
if (currentSection.value > previousSection.value) {
return 'logo-slide-left'
}
return 'logo-slide-right'
})
return { logoTransitionName }
}