Skip to content

Commit 96fad2c

Browse files
committed
refactor(cli): consolidate terminal breakpoints into reusable hook
- Create useTerminalBreakpoints hook (terminal equivalent of useMediaQuery) - Provides consistent breakpoint values across components - Replace manual breakpoint calculations in LoginModal - More maintainable and DRY code
1 parent 5eccb20 commit 96fad2c

File tree

2 files changed

+91
-23
lines changed

2 files changed

+91
-23
lines changed

cli/src/components/login-modal.tsx

Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useKeyboard, useRenderer } from '@opentui/react'
1+
import { useKeyboard } from '@opentui/react'
22
import { useMutation } from '@tanstack/react-query'
33
import open from 'open'
44
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
@@ -12,6 +12,7 @@ import {
1212
} from './login-modal-utils'
1313
import { TerminalLink } from './terminal-link'
1414
import { useLoginMutation } from '../hooks/use-auth-query'
15+
import { useTerminalBreakpoints } from '../hooks/use-terminal-breakpoints'
1516
import { generateLoginUrl, pollLoginStatus } from '../login/login-flow'
1617
import { copyTextToClipboard } from '../utils/clipboard'
1718
import { logger } from '../utils/logger'
@@ -50,7 +51,7 @@ export const LoginModal = ({
5051
theme,
5152
hasInvalidCredentials = false,
5253
}: LoginModalProps) => {
53-
const renderer = useRenderer()
54+
const breakpoints = useTerminalBreakpoints()
5455
const [loginUrl, setLoginUrl] = useState<string | null>(null)
5556
const [loading, setLoading] = useState(false)
5657
const [error, setError] = useState<string | null>(null)
@@ -329,14 +330,14 @@ export const LoginModal = ({
329330
useEffect(() => {
330331
const interval = setInterval(() => {
331332
setSheenPosition((prev) => {
332-
const modulo = Math.max(10, Math.min((renderer?.width || 80) - 4, 100))
333+
const modulo = Math.max(10, Math.min(breakpoints.width - 4, 100))
333334
const next = (prev + 1) % modulo
334335
return next
335336
})
336337
}, 150) // Update every 150ms for smooth animation with less CPU usage
337338

338339
return () => clearInterval(interval)
339-
}, [])
340+
}, [breakpoints.width])
340341

341342
// Determine if we're in light mode by checking background color luminance
342343
const isLightMode = useMemo(
@@ -368,19 +369,9 @@ export const LoginModal = ({
368369
// Memoize logo lines to prevent recalculation
369370
const logoLines = useMemo(() => parseLogoLines(LOGO), [])
370371

371-
// Calculate terminal width and height for responsive display
372-
const terminalWidth = renderer?.width || 80
373-
const terminalHeight = renderer?.height || 24
374-
const maxUrlWidth = Math.min(terminalWidth - 10, 100)
375-
376-
// Responsive breakpoints based on terminal height
377-
const isVerySmall = terminalHeight < 15 // Minimal UI
378-
const isSmall = terminalHeight >= 15 && terminalHeight < 20 // Compact UI
379-
const isMedium = terminalHeight >= 20 && terminalHeight < 30 // Standard UI
380-
const isLarge = terminalHeight >= 30 // Spacious UI
381-
382-
// Responsive breakpoints based on terminal width
383-
const isNarrow = terminalWidth < 60
372+
// Calculate responsive dimensions using breakpoints
373+
const { width, height, isVerySmall, isNarrow, isTall } = breakpoints
374+
const maxUrlWidth = Math.min(width - 10, 100)
384375

385376
// Dynamic spacing based on terminal size - compressed to prevent scrolling
386377
const containerPadding = isVerySmall ? 0 : 1
@@ -389,7 +380,7 @@ export const LoginModal = ({
389380
const sectionMarginBottom = isVerySmall ? 0 : 1
390381
const contentMaxWidth = Math.max(
391382
10,
392-
Math.min(terminalWidth - (containerPadding * 2 + 4), 80),
383+
Math.min(width - (containerPadding * 2 + 4), 80),
393384
)
394385

395386
const logoDisplayLines = useMemo(
@@ -398,23 +389,23 @@ export const LoginModal = ({
398389
)
399390

400391
// Show full logo on all terminal sizes as long as width allows
401-
const showFullLogo = contentMaxWidth >= 60
392+
const showFullLogo = isTall && contentMaxWidth >= 60
402393
// Show simple header on smaller terminals
403394
const showHeader = true
404395

405396
// Format URL for display (wrap if needed)
406397
return (
407398
<box
408399
position="absolute"
409-
left={Math.floor(terminalWidth * 0.05)}
400+
left={Math.floor(width * 0.05)}
410401
top={1}
411402
border
412403
borderStyle="double"
413404
borderColor={theme.statusAccent}
414405
style={{
415-
width: Math.floor(terminalWidth * 0.9),
416-
height: Math.min(Math.floor((renderer?.height || 24) - 2), 22),
417-
maxHeight: Math.min(Math.floor((renderer?.height || 24) - 2), 22),
406+
width: Math.floor(width * 0.9),
407+
height: Math.min(Math.floor(height - 2), 22),
408+
maxHeight: Math.min(Math.floor(height - 2), 22),
418409
backgroundColor: theme.background,
419410
padding: 0,
420411
overflow: 'hidden',
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { useRenderer } from '@opentui/react'
2+
import { useMemo } from 'react'
3+
4+
export interface TerminalBreakpoints {
5+
// Width-based
6+
width: number
7+
isNarrow: boolean // < 60 cols
8+
isMediumWidth: boolean // >= 60 && < 100
9+
isWide: boolean // >= 100 cols
10+
11+
// Height-based
12+
height: number
13+
isVerySmall: boolean // < 15 rows (minimal UI)
14+
isSmall: boolean // >= 15 && < 20 rows (compact UI)
15+
isMedium: boolean // >= 20 && < 30 rows (standard UI)
16+
isLarge: boolean // >= 30 rows (spacious UI)
17+
isTall: boolean // >= 20 rows (alias for backward compatibility)
18+
}
19+
20+
const WIDTH_BREAKPOINTS = {
21+
narrow: 60,
22+
mediumWidth: 100,
23+
} as const
24+
25+
const HEIGHT_BREAKPOINTS = {
26+
verySmall: 15,
27+
small: 20,
28+
medium: 30,
29+
} as const
30+
31+
/**
32+
* Hook to get responsive breakpoints based on terminal dimensions.
33+
* Similar to useMediaQuery in web apps, but for terminal-based UIs.
34+
*
35+
* @returns Object with terminal dimensions and boolean breakpoint flags
36+
*
37+
* @example
38+
* const { isNarrow, isVerySmall, width, height } = useTerminalBreakpoints()
39+
*
40+
* // Use breakpoints for conditional rendering
41+
* {isNarrow && <CompactView />}
42+
* {!isNarrow && <FullView />}
43+
*
44+
* // Use dimensions for calculations
45+
* const maxWidth = Math.min(width - 10, 100)
46+
*/
47+
export const useTerminalBreakpoints = (): TerminalBreakpoints => {
48+
const renderer = useRenderer()
49+
50+
return useMemo(() => {
51+
const width = renderer?.width || 80
52+
const height = renderer?.height || 24
53+
54+
return {
55+
width,
56+
height,
57+
58+
// Width breakpoints
59+
isNarrow: width < WIDTH_BREAKPOINTS.narrow,
60+
isMediumWidth:
61+
width >= WIDTH_BREAKPOINTS.narrow &&
62+
width < WIDTH_BREAKPOINTS.mediumWidth,
63+
isWide: width >= WIDTH_BREAKPOINTS.mediumWidth,
64+
65+
// Height breakpoints
66+
isVerySmall: height < HEIGHT_BREAKPOINTS.verySmall,
67+
isSmall:
68+
height >= HEIGHT_BREAKPOINTS.verySmall &&
69+
height < HEIGHT_BREAKPOINTS.small,
70+
isMedium:
71+
height >= HEIGHT_BREAKPOINTS.small &&
72+
height < HEIGHT_BREAKPOINTS.medium,
73+
isLarge: height >= HEIGHT_BREAKPOINTS.medium,
74+
isTall: height >= HEIGHT_BREAKPOINTS.small, // Alias for backward compatibility
75+
}
76+
}, [renderer?.width, renderer?.height])
77+
}

0 commit comments

Comments
 (0)