Swadhin Biswas feat: implement mervelas cli engine, built-in tools, and reactive UI · 4/3/2026
0358986
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
export type HorizontalScrollWindow = {
startIndex: number
endIndex: number
showLeftArrow: boolean
showRightArrow: boolean
}
/**
* Calculate the visible window of items that fit within available width,
* ensuring the selected item is always visible. Uses edge-based scrolling:
* the window only scrolls when the selected item would be outside the visible
* range, and positions the selected item at the edge (not centered).
*
* @param itemWidths - Array of item widths (each width should include separator if applicable)
* @param availableWidth - Total available width for items
* @param arrowWidth - Width of scroll indicator arrow (including space)
* @param selectedIdx - Index of selected item (must stay visible)
* @param firstItemHasSeparator - Whether first item's width includes a separator that should be ignored
* @returns Visible window bounds and whether to show scroll arrows
*/
export function calculateHorizontalScrollWindow(
itemWidths: number[],
availableWidth: number,
arrowWidth: number,
selectedIdx: number,
firstItemHasSeparator = true,
): HorizontalScrollWindow {
const totalItems = itemWidths.length
if (totalItems === 0) {
return {
startIndex: 0,
endIndex: 0,
showLeftArrow: false,
showRightArrow: false,
}
}
// Clamp selectedIdx to valid range
const clampedSelected = Math.max(0, Math.min(selectedIdx, totalItems - 1))
// If all items fit, show them all
const totalWidth = itemWidths.reduce((sum, w) => sum + w, 0)
if (totalWidth <= availableWidth) {
return {
startIndex: 0,
endIndex: totalItems,
showLeftArrow: false,
showRightArrow: false,
}
}
// Calculate cumulative widths for efficient range calculations
const cumulativeWidths: number[] = [0]
for (let i = 0; i < totalItems; i++) {
cumulativeWidths.push(cumulativeWidths[i]! + itemWidths[i]!)
}
// Helper to get width of range [start, end)
function rangeWidth(start: number, end: number): number {
const baseWidth = cumulativeWidths[end]! - cumulativeWidths[start]!
// When starting after index 0 and first item has separator baked in,
// subtract 1 because we don't render leading separator on first visible item
if (firstItemHasSeparator && start > 0) {
return baseWidth - 1
}
return baseWidth
}
// Calculate effective available width based on whether we'll show arrows
function getEffectiveWidth(start: number, end: number): number {
let width = availableWidth
if (start > 0) width -= arrowWidth // left arrow
if (end < totalItems) width -= arrowWidth // right arrow
return width
}
// Edge-based scrolling: Start from the beginning and only scroll when necessary
// First, calculate how many items fit starting from index 0
let startIndex = 0
let endIndex = 1
// Expand from start as much as possible
while (
endIndex < totalItems &&
rangeWidth(startIndex, endIndex + 1) <=
getEffectiveWidth(startIndex, endIndex + 1)
) {
endIndex++
}
// If selected is within visible range, we're done
if (clampedSelected >= startIndex && clampedSelected < endIndex) {
return {
startIndex,
endIndex,
showLeftArrow: startIndex > 0,
showRightArrow: endIndex < totalItems,
}
}
// Selected is outside visible range - need to scroll
if (clampedSelected >= endIndex) {
// Selected is to the right - scroll so selected is at the right edge
endIndex = clampedSelected + 1
startIndex = clampedSelected
// Expand left as much as possible (selected stays at right edge)
while (
startIndex > 0 &&
rangeWidth(startIndex - 1, endIndex) <=
getEffectiveWidth(startIndex - 1, endIndex)
) {
startIndex--
}
} else {
// Selected is to the left - scroll so selected is at the left edge
startIndex = clampedSelected
endIndex = clampedSelected + 1
// Expand right as much as possible (selected stays at left edge)
while (
endIndex < totalItems &&
rangeWidth(startIndex, endIndex + 1) <=
getEffectiveWidth(startIndex, endIndex + 1)
) {
endIndex++
}
}
return {
startIndex,
endIndex,
showLeftArrow: startIndex > 0,
showRightArrow: endIndex < totalItems,
}
}