Tab Bar Load Animation

Nucleic-Session: EEC60FAE-8368-49CA-9202-7026C12E7F8C
Co-authored-by: Nucleic <[email protected]>
This commit is contained in:
2026-07-05 18:31:25 -07:00
co-authored by Nucleic
parent a27f29acf2
commit f72f4297e1
@@ -74,10 +74,13 @@ struct CompactRootView: View {
} }
} }
/// Our own bottom tab bar for the compact shell — a plain `.bar`-material strip of five items, /// Our own bottom tab bar for the compact shell — a floating Liquid Glass capsule of five items,
/// standing in for the system tab bar so `CompactRootView` can animate its visibility itself /// standing in for the system tab bar so `CompactRootView` can animate its visibility itself (see
/// (see that view for why). The material bleeds into the home-indicator area; the items stay in /// that view for why) while keeping the iOS 26 floating-glass look. Real `glassEffect` on iOS 26;
/// the safe region. The Sessions item carries the "needs you" badge the system tab item used to. /// an ultra-thin material capsule with a hairline edge + soft shadow on earlier systems, matching
/// the app's `glassSurface`/`glassCircle`. It floats inset from the edges, above the home
/// indicator — it does not bleed to the screen edge. The Sessions item carries the "needs you"
/// badge the system tab item used to.
struct NucleicTabBar: View { struct NucleicTabBar: View {
@Binding var selection: Int @Binding var selection: Int
/// Sessions blocking on a human — the red badge on the Sessions item (was the tab item badge). /// Sessions blocking on a human — the red badge on the Sessions item (was the tab item badge).
@@ -98,15 +101,13 @@ struct NucleicTabBar: View {
itemButton(item) itemButton(item)
} }
} }
.padding(.top, 8) .padding(.horizontal, 6)
.padding(.bottom, 2) .padding(.vertical, 8)
// A hairline top separator, like the system bar's edge over scrolling content. .glassBar()
.overlay(alignment: .top) { // Float the capsule inset from the edges and just above the home indicator, rather than
Rectangle().fill(Color.primary.opacity(0.10)).frame(height: 0.5) // filling the bottom edge like the old opaque bar.
} .padding(.horizontal, 16)
// `.bar` is the material the system uses for tab/tool bars; bleed it into the bottom .padding(.bottom, 4)
// unsafe area so it reaches the screen edge under the home indicator.
.background(.bar, ignoresSafeAreaEdges: .bottom)
} }
private func itemButton(_ item: Item) -> some View { private func itemButton(_ item: Item) -> some View {
@@ -116,8 +117,8 @@ struct NucleicTabBar: View {
} label: { } label: {
VStack(spacing: 3) { VStack(spacing: 3) {
Image(systemName: item.icon) Image(systemName: item.icon)
.font(.system(size: 22, weight: selected ? .semibold : .regular)) .font(.system(size: 21, weight: selected ? .semibold : .regular))
.frame(height: 26) .frame(height: 24)
.overlay(alignment: .topTrailing) { .overlay(alignment: .topTrailing) {
if item.index == 1, needsYou > 0 { if item.index == 1, needsYou > 0 {
Text(needsYou > 99 ? "99+" : "\(needsYou)") Text(needsYou > 99 ? "99+" : "\(needsYou)")
@@ -133,6 +134,14 @@ struct NucleicTabBar: View {
} }
.foregroundStyle(selected ? Palette.accent : Color.secondary) .foregroundStyle(selected ? Palette.accent : Color.secondary)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.padding(.vertical, 4)
// The selected item rides a soft accent-tinted capsule, echoing the system floating
// bar's highlighted tab.
.background {
if selected {
Capsule().fill(Palette.accent.opacity(0.15))
}
}
.contentShape(Rectangle()) .contentShape(Rectangle())
} }
.buttonStyle(.plain) .buttonStyle(.plain)
@@ -141,6 +150,22 @@ struct NucleicTabBar: View {
} }
} }
private extension View {
/// The floating tab bar's Liquid Glass capsule backing — the capsule twin of `glassSurface`.
/// Real `glassEffect` on iOS 26; an ultra-thin material capsule with a hairline edge and soft
/// shadow on earlier systems so it still lifts off the content.
@ViewBuilder
func glassBar() -> some View {
if #available(iOS 26.0, *) {
glassEffect(.regular, in: .capsule)
} else {
background(.ultraThinMaterial, in: Capsule())
.overlay(Capsule().strokeBorder(Color.primary.opacity(0.08), lineWidth: 1))
.shadow(color: .black.opacity(0.18), radius: 10, y: 3)
}
}
}
// MARK: - Regular (iPad sidebar + detail) // MARK: - Regular (iPad sidebar + detail)
/// What the sidebar can select. Mirrors the Mac sidebar: a few global destinations plus the /// What the sidebar can select. Mirrors the Mac sidebar: a few global destinations plus the