feat(bar): make the profile carousel mouse-navigable

The horizontal paging carousel only responded to trackpad swipe; a mouse could
not move it. Add clickable prev/next arrows and make the page dots tappable --
each sets the scrollPosition binding (animated), so mouse users can page through
a provider's profiles. Trackpad swipe is unchanged.
This commit is contained in:
Tam Nhu Tran committed 2026-06-23 23:49:11 -04:00
1 parent a0a2dd00fa
commit f57eed8c34
1 file changed
+51 -10
+51 -10
View File
@@ -291,16 +291,11 @@ struct BarMenuView: View {
get: { selectedProfileByProvider[prov] ?? rows.first?.id },
set: { selectedProfileByProvider[prov] = $0 ?? rows.first?.id }))
.frame(height: carouselHeight(rows))
// Page dots — one per profile in this provider's carousel.
HStack(spacing: 6) {
ForEach(rows) { row in
let isCurrent = (selectedProfileByProvider[prov] ?? rows.first?.id) == row.id
Circle()
.fill(isCurrent ? theme.subscription : Color.secondary.opacity(0.3))
.frame(width: 6, height: 6)
}
}
.frame(maxWidth: .infinity, alignment: .center)
// Page controls — clickable prev/next arrows + dots so the carousel
// is navigable by MOUSE (click), not just by trackpad swipe.
// Programmatically setting the scrollPosition binding pages the
// ScrollView; withAnimation gives the same feel as a swipe.
carouselControls(prov: prov, rows: rows)
}
}
}
@@ -364,6 +359,52 @@ struct BarMenuView: View {
}
}
/// Prev/next arrows + clickable dots for a provider's profile carousel, so it
/// is navigable by MOUSE (click), not only by trackpad swipe. Selecting a page
/// sets the scrollPosition binding, which scrolls the ScrollView to that card.
@ViewBuilder private func carouselControls(prov: String, rows: [BarSummaryRow]) -> some View {
let currentId = selectedProfileByProvider[prov] ?? rows.first?.id
let curIdx = rows.firstIndex(where: { $0.id == currentId }) ?? 0
HStack(spacing: 7) {
pageArrow(systemName: "chevron.left", enabled: curIdx > 0) {
if curIdx > 0 { selectPage(prov, rows[curIdx - 1].id) }
}
ForEach(rows) { row in
Circle()
.fill(currentId == row.id ? theme.subscription : Color.secondary.opacity(0.3))
.frame(width: 6, height: 6)
.padding(4) // larger mouse hit target than the 6pt dot
.contentShape(Rectangle())
.onTapGesture { selectPage(prov, row.id) }
}
pageArrow(systemName: "chevron.right", enabled: curIdx < rows.count - 1) {
if curIdx < rows.count - 1 { selectPage(prov, rows[curIdx + 1].id) }
}
}
.frame(maxWidth: .infinity, alignment: .center)
}
/// Animate the carousel to the given profile card (mouse click or dot tap).
private func selectPage(_ prov: String, _ id: String) {
withAnimation(.easeInOut(duration: 0.2)) {
selectedProfileByProvider[prov] = id
}
}
@ViewBuilder private func pageArrow(
systemName: String, enabled: Bool, action: @escaping () -> Void
) -> some View {
Button(action: action) {
Image(systemName: systemName)
.font(.system(size: 9, weight: .bold))
.frame(width: 16, height: 16)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.disabled(!enabled)
.opacity(enabled ? 0.7 : 0.25)
}
/// Height of the tallest single card in a carousel — only one card is visible
/// at a time, so the paged frame is sized to fit it WITHOUT reserving blank
/// space beneath. Card = vertical padding (16) + title row (~22) + one bar per