|
1 | | -import React, { useMemo } from 'react' |
| 1 | +import React, { useEffect, useMemo, useState } from 'react' |
2 | 2 |
|
3 | 3 | import { LOGO, LOGO_SMALL, SHADOW_CHARS } from '../login/constants' |
4 | 4 | import { parseLogoLines } from '../login/utils' |
5 | 5 | import { IS_FREEBUFF } from '../utils/constants' |
| 6 | +import { useTheme } from './use-theme' |
| 7 | + |
| 8 | +const SUBTITLE_SHIMMER_STEPS = 10 |
| 9 | +const SUBTITLE_SHIMMER_INTERVAL_MS = 180 |
| 10 | +const SUBTITLE_SHIMMER_COLORS = { |
| 11 | + dark: { base: '#9EFC62', bright: '#CCFF99', peak: '#ffffff' }, |
| 12 | + light: { base: '#65A83E', bright: '#88D458', peak: '#ffffff' }, |
| 13 | +} as const |
6 | 14 |
|
7 | 15 | interface UseLogoOptions { |
8 | 16 | /** |
@@ -137,5 +145,54 @@ export const useLogo = ({ |
137 | 145 | ) |
138 | 146 | }, [rawLogoString, availableWidth, applySheenToChar, textColor, accentColor, blockColor]) |
139 | 147 |
|
140 | | - return { component, textBlock } |
| 148 | + // Freebuff subtitle: "The free coding agent" with shimmer wave on "free" |
| 149 | + const theme = useTheme() |
| 150 | + const [shimmerPos, setShimmerPos] = useState(0) |
| 151 | + |
| 152 | + useEffect(() => { |
| 153 | + if (!IS_FREEBUFF) return |
| 154 | + const interval = setInterval(() => { |
| 155 | + setShimmerPos(prev => (prev + 1) % SUBTITLE_SHIMMER_STEPS) |
| 156 | + }, SUBTITLE_SHIMMER_INTERVAL_MS) |
| 157 | + return () => clearInterval(interval) |
| 158 | + }, []) |
| 159 | + |
| 160 | + const componentWithSubtitle = useMemo(() => { |
| 161 | + if (!IS_FREEBUFF) return component |
| 162 | + |
| 163 | + const colors = SUBTITLE_SHIMMER_COLORS[theme.name] ?? SUBTITLE_SHIMMER_COLORS.dark |
| 164 | + |
| 165 | + // Calculate logo width to center the subtitle |
| 166 | + const subtitleText = 'The free coding agent' |
| 167 | + const logoLines = rawLogoString === 'CODEBUFF' || rawLogoString === 'FREEBUFF' |
| 168 | + ? [rawLogoString] |
| 169 | + : parseLogoLines(rawLogoString).map((line) => line.slice(0, availableWidth)) |
| 170 | + const logoWidth = Math.max(...logoLines.map((l) => l.length)) |
| 171 | + const padding = Math.max(0, Math.floor((logoWidth - subtitleText.length) / 2)) |
| 172 | + const pad = ' '.repeat(padding) |
| 173 | + |
| 174 | + const subtitle = ( |
| 175 | + <text style={{ wrapMode: 'none' }}> |
| 176 | + <span>{pad}</span> |
| 177 | + <span fg={theme.foreground}>The </span> |
| 178 | + <b> |
| 179 | + {'free'.split('').map((char, i) => { |
| 180 | + const distance = Math.abs(shimmerPos - 1 - i) |
| 181 | + const color = distance === 0 ? colors.peak : distance === 1 ? colors.bright : colors.base |
| 182 | + return <span key={i} fg={color}>{char}</span> |
| 183 | + })} |
| 184 | + </b> |
| 185 | + <span fg={theme.foreground}> coding agent</span> |
| 186 | + </text> |
| 187 | + ) |
| 188 | + |
| 189 | + return ( |
| 190 | + <> |
| 191 | + {component} |
| 192 | + {subtitle} |
| 193 | + </> |
| 194 | + ) |
| 195 | + }, [component, shimmerPos, theme.name, theme.foreground, rawLogoString, availableWidth]) |
| 196 | + |
| 197 | + return { component: componentWithSubtitle, textBlock } |
141 | 198 | } |
0 commit comments