mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-12 18:15:48 +00:00
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:
1 parent
a0a2dd00fa
commit
f57eed8c34
1 file changed
+51
-10
@@ -291,16 +291,11 @@ struct BarMenuView: View {
|
|||||||
get: { selectedProfileByProvider[prov] ?? rows.first?.id },
|
get: { selectedProfileByProvider[prov] ?? rows.first?.id },
|
||||||
set: { selectedProfileByProvider[prov] = $0 ?? rows.first?.id }))
|
set: { selectedProfileByProvider[prov] = $0 ?? rows.first?.id }))
|
||||||
.frame(height: carouselHeight(rows))
|
.frame(height: carouselHeight(rows))
|
||||||
// Page dots — one per profile in this provider's carousel.
|
// Page controls — clickable prev/next arrows + dots so the carousel
|
||||||
HStack(spacing: 6) {
|
// is navigable by MOUSE (click), not just by trackpad swipe.
|
||||||
ForEach(rows) { row in
|
// Programmatically setting the scrollPosition binding pages the
|
||||||
let isCurrent = (selectedProfileByProvider[prov] ?? rows.first?.id) == row.id
|
// ScrollView; withAnimation gives the same feel as a swipe.
|
||||||
Circle()
|
carouselControls(prov: prov, rows: rows)
|
||||||
.fill(isCurrent ? theme.subscription : Color.secondary.opacity(0.3))
|
|
||||||
.frame(width: 6, height: 6)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity, alignment: .center)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
/// 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
|
/// 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
|
/// space beneath. Card = vertical padding (16) + title row (~22) + one bar per
|
||||||
|
|||||||
Reference in new issue
Block a user