Liquid Glass Phase 1: segmented-selection tab bars and filter chips

First Liquid Glass adoption in Confab (deployment target is already
macOS 26.2, so no #available gating needed anywhere). Converts the two
confirmed-safe, non-scrolling segmented selectors:

- Settings' top-level 12-tab bar (tabButton) and yesterday's new MCP-tab
  sidebar (mcpSidebarRow): selected-state Color.blue.opacity(0.1)
  backgrounds become tinted .glassEffect(), wrapped in
  GlassEffectContainer. Shipped without morphing (@Namespace/
  glassEffectID) for now, since at most one item is visible at a time in
  steady state — morphing is a stretch goal only if wanted after seeing
  this.
- ModelSelectorView's filter/category/favorites/sort chip row: flat
  Color.opacity() pill backgrounds become .glassEffect(), all wrapped in
  one GlassEffectContainer for a real multi-element merge demo (several
  chips can be active simultaneously here, unlike the single-selection
  tab bars).

Deliberately NOT touched in this phase: anything inside a ScrollView/
List (formSection cards, chat bubbles, sidebar rows) per Apple's own
no-glass-in-scroll-views guidance, and the two ChatView anti-patterns
(Header/FooterView's .ultraThinMaterial) which need a bigger safeAreaBar
restructuring, planned as Phase 2.
This commit is contained in:
2026-08-21 14:38:25 +02:00
parent b8e1986cdb
commit ff26a57d26
2 changed files with 44 additions and 33 deletions
+9 -12
View File
@@ -96,6 +96,7 @@ struct ModelSelectorView: View {
} }
// Filters + Sort // Filters + Sort
GlassEffectContainer(spacing: 12) {
HStack(spacing: 12) { HStack(spacing: 12) {
FilterToggle(isOn: $filterVision, icon: "\u{1F441}\u{FE0F}", label: "Vision") FilterToggle(isOn: $filterVision, icon: "\u{1F441}\u{FE0F}", label: "Vision")
FilterToggle(isOn: $filterTools, icon: "\u{1F527}", label: "Tools") FilterToggle(isOn: $filterTools, icon: "\u{1F527}", label: "Tools")
@@ -120,13 +121,11 @@ struct ModelSelectorView: View {
.font(.caption) .font(.caption)
.padding(.horizontal, 10) .padding(.horizontal, 10)
.padding(.vertical, 6) .padding(.vertical, 6)
.background(selectedCategory != nil ? selectedCategory!.color.opacity(0.18) : Color.gray.opacity(0.1)) .glassEffect(
.foregroundColor(selectedCategory != nil ? selectedCategory!.color : .secondary) selectedCategory != nil ? .regular.tint(selectedCategory!.color) : .regular,
.cornerRadius(6) in: .rect(cornerRadius: 6)
.overlay(
RoundedRectangle(cornerRadius: 6)
.strokeBorder(selectedCategory != nil ? selectedCategory!.color.opacity(0.4) : Color.clear, lineWidth: 1)
) )
.foregroundColor(selectedCategory != nil ? selectedCategory!.color : .secondary)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.help("Filter by category") .help("Filter by category")
@@ -145,9 +144,8 @@ struct ModelSelectorView: View {
.font(.caption) .font(.caption)
.padding(.horizontal, 10) .padding(.horizontal, 10)
.padding(.vertical, 6) .padding(.vertical, 6)
.background(filterFavorites ? Color.yellow.opacity(0.25) : Color.gray.opacity(0.1)) .glassEffect(filterFavorites ? .regular.tint(.yellow) : .regular, in: .rect(cornerRadius: 6))
.foregroundColor(filterFavorites ? .yellow : .secondary) .foregroundColor(filterFavorites ? .yellow : .secondary)
.cornerRadius(6)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.help("Show favorites only") .help("Show favorites only")
@@ -174,13 +172,13 @@ struct ModelSelectorView: View {
.font(.caption) .font(.caption)
.padding(.horizontal, 10) .padding(.horizontal, 10)
.padding(.vertical, 6) .padding(.vertical, 6)
.background(sortOrder != .default ? Color.blue.opacity(0.2) : Color.gray.opacity(0.1)) .glassEffect(sortOrder != .default ? .regular.tint(.blue) : .regular, in: .rect(cornerRadius: 6))
.foregroundColor(sortOrder != .default ? .blue : .secondary) .foregroundColor(sortOrder != .default ? .blue : .secondary)
.cornerRadius(6)
} }
.menuStyle(.borderlessButton) .menuStyle(.borderlessButton)
.fixedSize() .fixedSize()
} }
}
.padding(.horizontal) .padding(.horizontal)
.padding(.bottom, 12) .padding(.bottom, 12)
@@ -299,9 +297,8 @@ struct FilterToggle: View {
.font(.caption) .font(.caption)
.padding(.horizontal, 10) .padding(.horizontal, 10)
.padding(.vertical, 6) .padding(.vertical, 6)
.background(isOn ? Color.blue.opacity(0.2) : Color.gray.opacity(0.1)) .glassEffect(isOn ? .regular.tint(.blue) : .regular, in: .rect(cornerRadius: 6))
.foregroundColor(isOn ? .blue : .secondary) .foregroundColor(isOn ? .blue : .secondary)
.cornerRadius(6)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.fixedSize() .fixedSize()
+35 -21
View File
@@ -226,22 +226,24 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
.padding(.bottom, 4) .padding(.bottom, 4)
// Icon toolbar Core | separator | Extras // Icon toolbar Core | separator | Extras
HStack(spacing: 0) { GlassEffectContainer(spacing: 8) {
tabButton(0, icon: "gear", label: "General") HStack(spacing: 0) {
tabButton(1, icon: "folder.badge.gearshape", label: "MCP") tabButton(0, icon: "gear", label: "General")
tabButton(2, icon: "paintbrush", label: "Appearance") tabButton(1, icon: "folder.badge.gearshape", label: "MCP")
tabButton(3, icon: "slider.horizontal.3", label: "Advanced") tabButton(2, icon: "paintbrush", label: "Appearance")
tabButton(3, icon: "slider.horizontal.3", label: "Advanced")
Divider().frame(height: 44).padding(.horizontal, 4) Divider().frame(height: 44).padding(.horizontal, 4)
tabButton(6, icon: "command", label: "Shortcuts") tabButton(6, icon: "command", label: "Shortcuts")
tabButton(7, icon: "brain", label: "Skills") tabButton(7, icon: "brain", label: "Skills")
tabButton(4, icon: "arrow.triangle.2.circlepath", label: "Sync") tabButton(4, icon: "arrow.triangle.2.circlepath", label: "Sync")
tabButton(5, icon: "envelope", label: "Email") tabButton(5, icon: "envelope", label: "Email")
tabButton(8, icon: "doc.text", label: "Paperless", beta: true) tabButton(8, icon: "doc.text", label: "Paperless", beta: true)
tabButton(9, icon: "icloud.and.arrow.up", label: "Backup") tabButton(9, icon: "icloud.and.arrow.up", label: "Backup")
tabButton(10, icon: "square.stack.3d.up", label: "Anytype") tabButton(10, icon: "square.stack.3d.up", label: "Anytype")
tabButton(11, icon: "server.rack", label: "Jarvis") tabButton(11, icon: "server.rack", label: "Jarvis")
}
} }
.padding(.horizontal, 16) .padding(.horizontal, 16)
.padding(.bottom, 12) .padding(.bottom, 12)
@@ -645,8 +647,13 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
} }
.padding(.horizontal, 10) .padding(.horizontal, 10)
.padding(.vertical, 7) .padding(.vertical, 7)
.background(selectedMCPSubsection == section ? Color.blue.opacity(0.1) : Color.clear) .background {
.clipShape(RoundedRectangle(cornerRadius: 8)) if selectedMCPSubsection == section {
Color.clear.glassEffect(.regular.tint(.blue), in: .rect(cornerRadius: 8))
} else {
Color.clear
}
}
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.foregroundStyle(selectedMCPSubsection == section ? .blue : .primary) .foregroundStyle(selectedMCPSubsection == section ? .blue : .primary)
@@ -655,9 +662,11 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
@ViewBuilder @ViewBuilder
private var mcpTabWithSidebar: some View { private var mcpTabWithSidebar: some View {
HStack(alignment: .top, spacing: 0) { HStack(alignment: .top, spacing: 0) {
VStack(alignment: .leading, spacing: 2) { GlassEffectContainer(spacing: 4) {
ForEach(MCPSubsection.visibleCases) { mcpSidebarRow($0) } VStack(alignment: .leading, spacing: 2) {
Spacer() ForEach(MCPSubsection.visibleCases) { mcpSidebarRow($0) }
Spacer()
}
} }
.padding(10) .padding(10)
.frame(width: 180) .frame(width: 180)
@@ -3330,8 +3339,13 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
.frame(minWidth: 55) .frame(minWidth: 55)
.padding(.vertical, 6) .padding(.vertical, 6)
.padding(.horizontal, 4) .padding(.horizontal, 4)
.background(selectedTab == tag ? Color.blue.opacity(0.1) : Color.clear) .background {
.clipShape(RoundedRectangle(cornerRadius: 8)) if selectedTab == tag {
Color.clear.glassEffect(.regular.tint(.blue), in: .rect(cornerRadius: 8))
} else {
Color.clear
}
}
} }
.buttonStyle(.plain) .buttonStyle(.plain)
} }