Skip to content

Commit 61cac8f

Browse files
committed
freebuff cli: add subtitle
1 parent b331f30 commit 61cac8f

File tree

1 file changed

+59
-2
lines changed

1 file changed

+59
-2
lines changed

cli/src/hooks/use-logo.tsx

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
1-
import React, { useMemo } from 'react'
1+
import React, { useEffect, useMemo, useState } from 'react'
22

33
import { LOGO, LOGO_SMALL, SHADOW_CHARS } from '../login/constants'
44
import { parseLogoLines } from '../login/utils'
55
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
614

715
interface UseLogoOptions {
816
/**
@@ -137,5 +145,54 @@ export const useLogo = ({
137145
)
138146
}, [rawLogoString, availableWidth, applySheenToChar, textColor, accentColor, blockColor])
139147

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 }
141198
}

0 commit comments

Comments
 (0)