-
Notifications
You must be signed in to change notification settings - Fork 519
Expand file tree
/
Copy pathsegmented-control.tsx
More file actions
187 lines (170 loc) · 5.27 KB
/
segmented-control.tsx
File metadata and controls
187 lines (170 loc) · 5.27 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
import React, { useState } from 'react'
import stringWidth from 'string-width'
import { useTheme } from '../hooks/use-theme'
import { Button } from './button'
import type { ChatTheme } from '../types/theme-system'
export interface Segment {
id: string
label: string
isBold?: boolean
isSelected?: boolean
defaultHighlighted?: boolean // Highlighted when nothing else is hovered
disabled?: boolean // Gray out and de-emphasize disabled items
}
/**
* SegmentedControlProps
*
* Renders a bordered segmented toggle. Pure UI; all behavior is driven by
* the parent via callbacks.
*/
interface SegmentedControlProps {
segments: Segment[]
onSegmentClick?: (id: string) => void
onMouseOver?: () => void
onMouseOut?: () => void
}
export const SegmentedControl = ({
segments,
onSegmentClick,
onMouseOver,
onMouseOut,
}: SegmentedControlProps) => {
const theme = useTheme()
const [hoveredId, setHoveredId] = useState<string | null>(null)
const [hasHoveredSinceOpen, setHasHoveredSinceOpen] = useState(false)
const processedSegments = processSegments(
segments,
hoveredId,
hasHoveredSinceOpen,
theme,
)
const hoveredIndex = hoveredId
? processedSegments.findIndex((s) => s.id === hoveredId)
: processedSegments.length - 1
return (
<box
style={{
flexDirection: 'row',
gap: 0,
backgroundColor: 'transparent',
}}
onMouseOver={onMouseOver}
onMouseOut={() => {
setHoveredId(null)
onMouseOut && onMouseOut()
}}
>
{/* Segments rendered with dynamic left/right edges based on hover */}
{processedSegments.map((seg, idx) => {
const leftOfHovered = idx <= hoveredIndex
const rightOfHovered = idx >= hoveredIndex
return (
<React.Fragment key={seg.id}>
{leftOfHovered ? (
<box style={{ flexDirection: 'column', gap: 0 }}>
<text fg={seg.frameColor} selectable={false}>╭</text>
<text fg={seg.frameColor} selectable={false}>│</text>
<text fg={seg.frameColor} selectable={false}>╰</text>
</box>
) : null}
<Button
onClick={() => onSegmentClick && onSegmentClick(seg.id)}
onMouseOver={() => {
setHoveredId(seg.id)
setHasHoveredSinceOpen(true)
}}
style={{
flexDirection: 'column',
gap: 0,
width: seg.width,
minWidth: seg.width,
}}
>
<text fg={seg.frameColor}>{seg.topBorder}</text>
<text fg={seg.textColor}>
{seg.isItalic ? (
<i>{seg.content}</i>
) : seg.isBold ? (
<b>{seg.content}</b>
) : (
seg.content
)}
</text>
<text fg={seg.frameColor}>{seg.bottomBorder}</text>
</Button>
{rightOfHovered ? (
<box style={{ flexDirection: 'column', gap: 0 }}>
<text fg={seg.frameColor} selectable={false}>╮</text>
<text fg={seg.frameColor} selectable={false}>│</text>
<text fg={seg.frameColor} selectable={false}>╯</text>
</box>
) : null}
</React.Fragment>
)
})}
</box>
)
}
export type ProcessedSegment = {
id: string
topBorder: string
content: string
bottomBorder: string
frameColor: string
leftBorderColor: string
textColor: string
isHovered: boolean
isBold: boolean
isItalic: boolean
label: string
width: number
}
/**
* Pure function that maps input segments + UI state to render-ready
* segment descriptors. This is exported for unit testing.
*/
export const processSegments = (
segments: Segment[],
hoveredId: string | null,
hasHoveredSinceOpen: boolean,
theme: ChatTheme,
): ProcessedSegment[] => {
return segments.map((seg) => {
// Normalized flags
const isDisabled = !!seg.disabled
const isSelected = !!seg.isSelected
const defaultHL = !!seg.defaultHighlighted
// Hover and highlight state
const canHover = !isSelected || defaultHL
const isHovered = hoveredId === seg.id && canHover
const isDefaultHighlighted = defaultHL && !hasHoveredSinceOpen
const isHighlighted = isHovered || isDefaultHighlighted
// Emphasis
const isBold = !!(seg.isBold || isHovered || (isSelected && isHighlighted))
// Colors
const frameColor = isHighlighted ? theme.foreground : theme.border
const textMuted = isDisabled || (isSelected && !isHighlighted)
const textColor = textMuted ? theme.muted : theme.foreground
// Content + metrics
const content = ` ${seg.label} `
const width = stringWidth(content)
const horizontal = '─'.repeat(width)
// Return render-ready descriptor
// - Computed (complex conditions): frameColor, textColor, isBold
// - Inlined (simple): isItalic (disabled), leftBorderColor (= frameColor)
return {
id: seg.id,
topBorder: horizontal,
content,
bottomBorder: horizontal,
frameColor,
leftBorderColor: frameColor,
textColor,
isHovered,
isBold,
isItalic: isDisabled,
label: seg.label,
width,
}
})
}