From 195e157e1a63b27a3bdf2917b00a39bceb667e1b Mon Sep 17 00:00:00 2001 From: James Magahern Date: Mon, 4 May 2026 21:07:55 -0700 Subject: [PATCH] ios: sidebar swipe --- .../Sources/Sybil/SybilPhoneShellView.swift | 544 +++++++++++------- .../Sources/Sybil/SybilSidebarView.swift | 15 +- .../Sources/Sybil/SybilWorkspaceView.swift | 2 +- 3 files changed, 342 insertions(+), 219 deletions(-) diff --git a/ios/Packages/Sybil/Sources/Sybil/SybilPhoneShellView.swift b/ios/Packages/Sybil/Sources/Sybil/SybilPhoneShellView.swift index bef59df..adfc540 100644 --- a/ios/Packages/Sybil/Sources/Sybil/SybilPhoneShellView.swift +++ b/ios/Packages/Sybil/Sources/Sybil/SybilPhoneShellView.swift @@ -22,75 +22,60 @@ enum PhoneRoute: Hashable { struct SybilPhoneShellView: View { @Bindable var viewModel: SybilViewModel - @State private var path: [PhoneRoute] = [] + @State private var route: PhoneRoute = .draftChat @Environment(\.scenePhase) private var scenePhase @State private var shouldRefreshOnForeground = false @State private var composerFocusRequest = 0 @State private var phoneStackWidth: CGFloat = BackSwipeMetrics.referenceWidth - @State private var backSwipeOffset: CGFloat = 0 - @State private var backSwipeCompletionOffset: CGFloat = 0 - @State private var backSwipeIsActive = false - @State private var backSwipeIsCompleting = false - @State private var backSwipeHasLatched = false + @State private var isSidebarOverlayPresented = false + @State private var sidebarSwipeOffset: CGFloat = 0 + @State private var sidebarSwipeIsActive = false + @State private var sidebarSwipeIsCompleting = false + @State private var sidebarSwipeHasLatched = false + @State private var sidebarHighlightSelection: SidebarSelection? + @State private var sidebarHighlightClearTask: Task? + @State private var openingSelectionRequestID: UUID? - private var canRecognizeBackSwipe: Bool { - !path.isEmpty && !backSwipeIsCompleting + private var canRecognizeSidebarSwipe: Bool { + !isSidebarOverlayPresented && !sidebarSwipeIsCompleting } - private var backSwipeVisualOffset: CGFloat { - backSwipeOffset + backSwipeCompletionOffset + private var sidebarOverlayProgress: CGFloat { + if isSidebarOverlayPresented { + return 1 + } + + return SidebarOverlaySwipeMetrics.progress( + for: sidebarSwipeOffset, + width: phoneStackWidth + ) + } + + private var shouldRenderSidebarOverlay: Bool { + isSidebarOverlayPresented || + sidebarSwipeIsActive || + sidebarSwipeIsCompleting || + sidebarOverlayProgress > 0.001 + } + + private var currentRouteSelection: SidebarSelection? { + switch route { + case let .chat(chatID): + return .chat(chatID) + case let .search(searchID): + return .search(searchID) + case .draftChat, .draftSearch, .settings: + return nil + } + } + + private var highlightedSidebarSelection: SidebarSelection? { + sidebarHighlightSelection ?? currentRouteSelection } var body: some View { GeometryReader { proxy in - ZStack(alignment: .topLeading) { - SybilPhoneSidebarRoot(viewModel: viewModel, path: $path) - .safeAreaInset(edge: .top, spacing: 0) { - phoneRootTopBar - } - .zIndex(0) - - if let route = path.last { - SybilPhoneDestinationView( - viewModel: viewModel, - composerFocusRequest: $composerFocusRequest, - route: route, - onRequestBack: requestBack, - onRequestNewChat: startNewChatFromDestination - ) - .background(SybilTheme.background) - .offset(x: backSwipeVisualOffset) - .shadow( - color: backSwipeVisualOffset > 0 ? Color.black.opacity(0.34) : Color.clear, - radius: backSwipeVisualOffset > 0 ? 18 : 0, - x: -8, - y: 0 - ) - .transition(.move(edge: .trailing)) - .zIndex(1) - .background { - WorkspaceSwipePanInstaller( - direction: .right, - isEnabled: canRecognizeBackSwipe, - onBegan: { width in - beginBackSwipe(containerWidth: width) - }, - onChanged: { translationX, width in - updateBackSwipe(with: translationX, containerWidth: width) - }, - onEnded: { translationX, width, velocityX, didFinish in - finishBackSwipe( - translationX: translationX, - containerWidth: width, - velocityX: velocityX, - didFinish: didFinish - ) - } - ) - .frame(maxWidth: .infinity, maxHeight: .infinity) - } - } - } + phoneStack(width: proxy.size.width) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) .onAppear { updatePhoneStackWidth(proxy.size.width) @@ -100,13 +85,8 @@ struct SybilPhoneShellView: View { } } .tint(SybilTheme.primary) - .animation(.easeOut(duration: 0.22), value: path.last) - .onChange(of: path) { _, nextPath in - guard nextPath.isEmpty else { - return - } - resetBackSwipe(animated: false) - } + .animation(.easeOut(duration: 0.22), value: route) + .animation(.easeOut(duration: 0.18), value: isSidebarOverlayPresented) .onChange(of: scenePhase) { _, nextPhase in switch nextPhase { case .background: @@ -120,8 +100,8 @@ struct SybilPhoneShellView: View { shouldRefreshOnForeground = false Task { await viewModel.refreshAfterAppBecameActive( - refreshCollections: path.isEmpty, - refreshSelection: !path.isEmpty && viewModel.hasRefreshableSelection + refreshCollections: isSidebarOverlayPresented, + refreshSelection: !isSidebarOverlayPresented && viewModel.hasRefreshableSelection ) } case .inactive: @@ -133,16 +113,117 @@ struct SybilPhoneShellView: View { } } - private var phoneRootTopBar: some View { - HStack { + private func phoneStack(width: CGFloat) -> some View { + ZStack(alignment: .topLeading) { + phoneWorkspaceLayer + .zIndex(0) + + phoneSidebarOverlayLayer(width: width) + .zIndex(1) + } + } + + private var phoneWorkspaceLayer: some View { + SybilPhoneDestinationView( + viewModel: viewModel, + composerFocusRequest: $composerFocusRequest, + route: route, + onRequestBack: { _ in showSidebarOverlay() }, + onRequestNewChat: sidebarWorkspaceNewChatAction, + onShowSidebar: showSidebarOverlay + ) + .background(SybilTheme.background) + .blur(radius: SidebarOverlaySwipeMetrics.workspaceBlurRadius(for: sidebarOverlayProgress)) + .opacity(SidebarOverlaySwipeMetrics.workspaceOpacity(for: sidebarOverlayProgress)) + .allowsHitTesting(!shouldRenderSidebarOverlay) + .background { + sidebarSwipeInstaller + } + } + + private func phoneSidebarOverlayLayer(width: CGFloat) -> some View { + VStack(spacing: 0) { + phoneOverlayTopBar + + SybilPhoneSidebarRoot( + viewModel: viewModel, + highlightedSelection: highlightedSidebarSelection, + onSelect: openSidebarSelection, + onRoute: showRouteAndClearSidebarHighlight + ) + } + .opacity(sidebarOverlayProgress) + .blur(radius: SidebarOverlaySwipeMetrics.overlayBlurRadius(for: sidebarOverlayProgress)) + .offset(x: SidebarOverlaySwipeMetrics.overlayOffset(for: sidebarOverlayProgress, width: width)) + .allowsHitTesting(isSidebarOverlayPresented) + .accessibilityHidden(!isSidebarOverlayPresented) + } + + private var sidebarSwipeInstaller: some View { + WorkspaceSwipePanInstaller( + direction: .right, + isEnabled: canRecognizeSidebarSwipe, + onBegan: { width in + beginSidebarSwipe(containerWidth: width) + }, + onChanged: { translationX, width in + updateSidebarSwipe(with: translationX, containerWidth: width) + }, + onEnded: { translationX, width, velocityX, didFinish in + finishSidebarSwipe( + translationX: translationX, + containerWidth: width, + velocityX: velocityX, + didFinish: didFinish + ) + } + ) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + + private var sidebarWorkspaceNewChatAction: (() -> Void)? { + guard !isSidebarOverlayPresented else { + return nil + } + + return { + startNewChatFromDestination() + } + } + + private var phoneOverlayTopBar: some View { + HStack(spacing: 12) { SybilWordmark(size: 21) Spacer() + + Button { + hideSidebarOverlay() + } label: { + Image(systemName: "chevron.right.2") + .font(.system(size: 21, weight: .bold)) + .foregroundStyle(SybilTheme.text) + .frame(width: 54, height: 54) + .background( + Circle() + .fill(.ultraThinMaterial) + .overlay( + Circle() + .fill(SybilTheme.surface.opacity(0.76)) + ) + ) + .overlay( + Circle() + .stroke(SybilTheme.border.opacity(0.64), lineWidth: 1) + ) + } + .buttonStyle(.plain) + .accessibilityLabel("Hide conversations") } .padding(.horizontal, 16) .padding(.top, 10) .padding(.bottom, 12) .background { - SybilTheme.panelGradient + SybilPhoneOverlayBlurBand(edge: .top) .ignoresSafeArea(edges: .top) } } @@ -151,62 +232,98 @@ struct SybilPhoneShellView: View { phoneStackWidth = max(width, 1) } - private func requestBack(animateNavigation: Bool = true) { - guard !path.isEmpty, !backSwipeIsCompleting else { - return - } - - if animateNavigation { - Task { - await completeBackSwipe(containerWidth: phoneStackWidth, releaseVelocityX: 0) - } - } else { - popRoute(disablesAnimations: true) - resetBackSwipe(animated: false) - } - } - private func startNewChatFromDestination() { viewModel.startNewChat() composerFocusRequest += 1 - replaceTopRoute(with: .draftChat) + showRoute(.draftChat) } - private func replaceTopRoute(with route: PhoneRoute) { - if path.isEmpty { + private func showRoute(_ nextRoute: PhoneRoute) { + let update = { + route = nextRoute + } + + if isSidebarOverlayPresented { withAnimation(.easeOut(duration: 0.22)) { - path = [route] + update() + isSidebarOverlayPresented = false } } else { - path[path.index(before: path.endIndex)] = route + update() } + + resetSidebarSwipe(animated: false) } - private func popRoute(disablesAnimations: Bool) { - let pop = { - guard !path.isEmpty else { + private func showRouteAndClearSidebarHighlight(_ nextRoute: PhoneRoute) { + showRoute(nextRoute) + clearSidebarHighlight() + } + + private func showSidebarOverlay() { + withAnimation(.easeOut(duration: 0.18)) { + isSidebarOverlayPresented = true + } + resetSidebarSwipe(animated: false) + } + + private func hideSidebarOverlay() { + withAnimation(.easeOut(duration: 0.18)) { + isSidebarOverlayPresented = false + } + resetSidebarSwipe(animated: false) + } + + private func openSidebarSelection(_ selection: SidebarSelection) { + if openingSelectionRequestID != nil, sidebarHighlightSelection == selection { + return + } + + let requestID = UUID() + openingSelectionRequestID = requestID + setSidebarHighlight(selection) + + Task { + await viewModel.selectForNavigation(selection) + guard openingSelectionRequestID == requestID else { return } - _ = path.removeLast() - } - if disablesAnimations { - var transaction = Transaction() - transaction.disablesAnimations = true - withTransaction(transaction) { - pop() - } - } else { - withAnimation(.easeOut(duration: 0.22)) { - pop() - } + showRoute(PhoneRoute.from(selection: selection)) + openingSelectionRequestID = nil + clearSidebarHighlight(selection, after: .milliseconds(260)) } } - private func beginBackSwipe(containerWidth: CGFloat) { + private func setSidebarHighlight(_ selection: SidebarSelection) { + sidebarHighlightClearTask?.cancel() + sidebarHighlightSelection = selection + } + + private func clearSidebarHighlight(_ selection: SidebarSelection, after delay: Duration) { + sidebarHighlightClearTask?.cancel() + sidebarHighlightClearTask = Task { @MainActor in + try? await Task.sleep(for: delay) + guard !Task.isCancelled, + sidebarHighlightSelection == selection, + openingSelectionRequestID == nil else { + return + } + sidebarHighlightSelection = nil + } + } + + private func clearSidebarHighlight() { + sidebarHighlightClearTask?.cancel() + openingSelectionRequestID = nil + sidebarHighlightSelection = nil + } + + private func beginSidebarSwipe(containerWidth: CGFloat) { let update = { - backSwipeIsActive = true - backSwipeHasLatched = false + phoneStackWidth = max(containerWidth, 1) + sidebarSwipeIsActive = true + sidebarSwipeHasLatched = false } var transaction = Transaction() @@ -214,97 +331,79 @@ struct SybilPhoneShellView: View { withTransaction(transaction, update) } - private func updateBackSwipe(with rawTranslation: CGFloat, containerWidth: CGFloat) { - let nextOffset = BackSwipeMetrics.clampedOffset(for: rawTranslation, width: containerWidth) - let nextLatched = BackSwipeMetrics.isLatched( + private func updateSidebarSwipe(with rawTranslation: CGFloat, containerWidth: CGFloat) { + let nextOffset = SidebarOverlaySwipeMetrics.clampedOffset(for: rawTranslation, width: containerWidth) + let nextLatched = SidebarOverlaySwipeMetrics.isLatched( offset: nextOffset, width: containerWidth, - isCurrentlyLatched: backSwipeHasLatched + isCurrentlyLatched: sidebarSwipeHasLatched ) var transaction = Transaction() transaction.disablesAnimations = true withTransaction(transaction) { - backSwipeOffset = nextOffset - backSwipeHasLatched = nextLatched + phoneStackWidth = max(containerWidth, 1) + sidebarSwipeOffset = nextOffset + sidebarSwipeHasLatched = nextLatched } } - private func finishBackSwipe( + private func finishSidebarSwipe( translationX: CGFloat, containerWidth: CGFloat, velocityX: CGFloat, didFinish: Bool ) { - guard backSwipeIsActive else { - resetBackSwipe(animated: false) + guard sidebarSwipeIsActive else { + resetSidebarSwipe(animated: false) return } - let finalOffset = BackSwipeMetrics.clampedOffset(for: translationX, width: containerWidth) - let finalLatched = BackSwipeMetrics.isLatched( + let finalOffset = SidebarOverlaySwipeMetrics.clampedOffset(for: translationX, width: containerWidth) + let finalLatched = SidebarOverlaySwipeMetrics.isLatched( offset: finalOffset, width: containerWidth, - isCurrentlyLatched: backSwipeHasLatched + isCurrentlyLatched: sidebarSwipeHasLatched ) - updateBackSwipe(with: translationX, containerWidth: containerWidth) + updateSidebarSwipe(with: translationX, containerWidth: containerWidth) - if didFinish && BackSwipeMetrics.shouldComplete( + if didFinish && SidebarOverlaySwipeMetrics.shouldComplete( offset: finalOffset, velocityX: velocityX, width: containerWidth, isLatched: finalLatched ) { - Task { - await completeBackSwipe(containerWidth: containerWidth, releaseVelocityX: velocityX) - } + completeSidebarSwipe() return } - resetBackSwipe(animated: true, velocityX: velocityX) + resetSidebarSwipe(animated: true, velocityX: velocityX) } - @MainActor - private func completeBackSwipe(containerWidth: CGFloat, releaseVelocityX: CGFloat) async { - guard !path.isEmpty else { - resetBackSwipe(animated: false) - return - } - guard !backSwipeIsCompleting else { + private func completeSidebarSwipe() { + guard !sidebarSwipeIsCompleting else { return } - backSwipeIsCompleting = true - let targetOffset = BackSwipeMetrics.completionTargetOffset(for: containerWidth) - - withAnimation( - BackSwipeMetrics.springAnimation( - currentOffset: backSwipeOffset, - targetOffset: targetOffset, - velocityX: releaseVelocityX - ) - ) { - backSwipeCompletionOffset = targetOffset - backSwipeOffset + sidebarSwipeIsCompleting = true + withAnimation(.easeOut(duration: 0.18)) { + isSidebarOverlayPresented = true } - - try? await Task.sleep(for: .milliseconds(BackSwipeMetrics.completionAnimationDelayMs)) - popRoute(disablesAnimations: true) - resetBackSwipe(animated: false) + resetSidebarSwipe(animated: false) } - private func resetBackSwipe(animated: Bool, velocityX: CGFloat = 0) { - let currentOffset = backSwipeOffset + backSwipeCompletionOffset + private func resetSidebarSwipe(animated: Bool, velocityX: CGFloat = 0) { + let currentOffset = sidebarSwipeOffset let reset = { - backSwipeOffset = 0 - backSwipeCompletionOffset = 0 - backSwipeIsActive = false - backSwipeIsCompleting = false - backSwipeHasLatched = false + sidebarSwipeOffset = 0 + sidebarSwipeIsActive = false + sidebarSwipeIsCompleting = false + sidebarSwipeHasLatched = false } if animated { withAnimation( - BackSwipeMetrics.springAnimation( + SidebarOverlaySwipeMetrics.springAnimation( currentOffset: currentOffset, targetOffset: 0, velocityX: velocityX @@ -318,31 +417,79 @@ struct SybilPhoneShellView: View { } } -private struct SybilPhoneSidebarRoot: View { - @Bindable var viewModel: SybilViewModel - @Binding var path: [PhoneRoute] - @State private var openingSelection: SidebarSelection? - @State private var openingRequestID: UUID? +private enum SidebarOverlaySwipeMetrics { + static func clampedOffset(for rawTranslation: CGFloat, width: CGFloat) -> CGFloat { + BackSwipeMetrics.clampedOffset(for: rawTranslation, width: width) + } - private var highlightedSelection: SidebarSelection? { - if let openingSelection { - return openingSelection - } + static func progress(for offset: CGFloat, width: CGFloat) -> CGFloat { + BackSwipeMetrics.progress(for: offset, width: width) + } - guard let route = path.last else { - return nil - } + static func isLatched(offset: CGFloat, width: CGFloat, isCurrentlyLatched: Bool = false) -> Bool { + BackSwipeMetrics.isLatched(offset: offset, width: width, isCurrentlyLatched: isCurrentlyLatched) + } - switch route { - case let .chat(chatID): - return .chat(chatID) - case let .search(searchID): - return .search(searchID) - case .draftChat, .draftSearch, .settings: - return nil + static func shouldComplete(offset: CGFloat, velocityX: CGFloat, width: CGFloat, isLatched: Bool) -> Bool { + BackSwipeMetrics.shouldComplete(offset: offset, velocityX: velocityX, width: width, isLatched: isLatched) + } + + static func springAnimation(currentOffset: CGFloat, targetOffset: CGFloat, velocityX: CGFloat) -> Animation { + BackSwipeMetrics.springAnimation(currentOffset: currentOffset, targetOffset: targetOffset, velocityX: velocityX) + } + + static func overlayOffset(for progress: CGFloat, width: CGFloat) -> CGFloat { + -(1 - min(max(progress, 0), 1)) * min(max(width * 0.18, 44), 76) + } + + static func overlayBlurRadius(for progress: CGFloat) -> CGFloat { + (1 - min(max(progress, 0), 1)) * 18 + } + + static func workspaceBlurRadius(for progress: CGFloat) -> CGFloat { + min(max(progress, 0), 1) * 14 + } + + static func workspaceOpacity(for progress: CGFloat) -> CGFloat { + 1 - (min(max(progress, 0), 1) * 0.22) + } +} + +private struct SybilPhoneOverlayBlurBand: View { + var edge: VerticalEdge + + var body: some View { + ZStack { + Rectangle() + .fill(.ultraThinMaterial) + .opacity(0.34) + + Rectangle() + .fill( + LinearGradient( + colors: gradientColors, + startPoint: edge == .top ? .top : .bottom, + endPoint: edge == .top ? .bottom : .top + ) + ) } } + private var gradientColors: [Color] { + [ + Color.black.opacity(0.94), + SybilTheme.background.opacity(0.78), + Color.black.opacity(0) + ] + } +} + +private struct SybilPhoneSidebarRoot: View { + @Bindable var viewModel: SybilViewModel + var highlightedSelection: SidebarSelection? + var onSelect: (SidebarSelection) -> Void + var onRoute: (PhoneRoute) -> Void + var body: some View { VStack(spacing: 0) { if let errorMessage = viewModel.errorMessage { @@ -363,7 +510,7 @@ private struct SybilPhoneSidebarRoot: View { highlightedSelection == item.selection }, onSelect: { item in - open(item.selection) + onSelect(item.selection) } ) } @@ -380,22 +527,20 @@ private struct SybilPhoneSidebarRoot: View { HStack(spacing: 12) { toolbarIconButton(systemImage: "gearshape", accessibilityLabel: "Settings") { - clearOpeningSelection() - showRoute(.settings) + viewModel.openSettings() + onRoute(.settings) } Spacer() toolbarIconButton(systemImage: "magnifyingglass", accessibilityLabel: "New search") { - clearOpeningSelection() viewModel.startNewSearch() - showRoute(.draftSearch) + onRoute(.draftSearch) } toolbarIconButton(systemImage: "plus", accessibilityLabel: "New chat", isPrimary: true) { - clearOpeningSelection() viewModel.startNewChat() - showRoute(.draftChat) + onRoute(.draftChat) } } .padding(.horizontal, 18) @@ -431,36 +576,6 @@ private struct SybilPhoneSidebarRoot: View { .buttonStyle(.plain) .accessibilityLabel(accessibilityLabel) } - - private func clearOpeningSelection() { - openingRequestID = nil - openingSelection = nil - } - - private func showRoute(_ route: PhoneRoute) { - withAnimation(.easeOut(duration: 0.22)) { - path = [route] - } - } - - private func open(_ selection: SidebarSelection) { - guard openingSelection != selection else { - return - } - - let requestID = UUID() - openingRequestID = requestID - openingSelection = selection - Task { - await viewModel.selectForNavigation(selection) - guard openingRequestID == requestID else { - return - } - showRoute(PhoneRoute.from(selection: selection)) - openingRequestID = nil - openingSelection = nil - } - } } private struct SybilPhoneDestinationView: View { @@ -468,12 +583,15 @@ private struct SybilPhoneDestinationView: View { @Binding var composerFocusRequest: Int let route: PhoneRoute let onRequestBack: (_ animateNavigation: Bool) -> Void - let onRequestNewChat: () -> Void + let onRequestNewChat: (() -> Void)? + let onShowSidebar: () -> Void var body: some View { SybilWorkspaceView( viewModel: viewModel, composerFocusRequest: composerFocusRequest, + navigationLeadingControl: .showSidebar, + onShowSidebar: onShowSidebar, onRequestBack: onRequestBack, onRequestNewChat: onRequestNewChat ) diff --git a/ios/Packages/Sybil/Sources/Sybil/SybilSidebarView.swift b/ios/Packages/Sybil/Sources/Sybil/SybilSidebarView.swift index 67a70ad..780b24f 100644 --- a/ios/Packages/Sybil/Sources/Sybil/SybilSidebarView.swift +++ b/ios/Packages/Sybil/Sources/Sybil/SybilSidebarView.swift @@ -172,6 +172,10 @@ struct SybilSidebarRow: View { var item: SidebarItem var isSelected: Bool + private var isHighlighted: Bool { + isSelected + } + private var iconName: String { switch item.kind { case .chat: return "message" @@ -184,14 +188,14 @@ struct SybilSidebarRow: View { HStack(spacing: 8) { Image(systemName: iconName) .font(.system(size: 12, weight: .semibold)) - .foregroundStyle(isSelected ? SybilTheme.accent : SybilTheme.textMuted) + .foregroundStyle(isHighlighted ? SybilTheme.accent : SybilTheme.textMuted) .frame(width: 22, height: 22) .background( RoundedRectangle(cornerRadius: 7) - .fill(isSelected ? SybilTheme.accent.opacity(0.12) : SybilTheme.surface.opacity(0.72)) + .fill(isHighlighted ? SybilTheme.accent.opacity(0.12) : SybilTheme.surface.opacity(0.72)) .overlay( RoundedRectangle(cornerRadius: 7) - .stroke(isSelected ? SybilTheme.accent.opacity(0.36) : SybilTheme.border.opacity(0.72), lineWidth: 1) + .stroke(isHighlighted ? SybilTheme.accent.opacity(0.36) : SybilTheme.border.opacity(0.72), lineWidth: 1) ) ) @@ -229,12 +233,13 @@ struct SybilSidebarRow: View { .frame(maxWidth: .infinity, alignment: .leading) .background( RoundedRectangle(cornerRadius: 12) - .fill(isSelected ? SybilTheme.selectedRowGradient : LinearGradient(colors: [SybilTheme.surface.opacity(0.56), SybilTheme.surface.opacity(0.36)], startPoint: .topLeading, endPoint: .bottomTrailing)) + .fill(isHighlighted ? SybilTheme.selectedRowGradient : LinearGradient(colors: [SybilTheme.surface.opacity(0.56), SybilTheme.surface.opacity(0.36)], startPoint: .topLeading, endPoint: .bottomTrailing)) ) .overlay( RoundedRectangle(cornerRadius: 12) - .stroke(isSelected ? SybilTheme.primary.opacity(0.55) : SybilTheme.border.opacity(0.72), lineWidth: 1) + .stroke(isHighlighted ? SybilTheme.primary.opacity(0.55) : SybilTheme.border.opacity(0.72), lineWidth: 1) ) + .contentShape(RoundedRectangle(cornerRadius: 12)) } } diff --git a/ios/Packages/Sybil/Sources/Sybil/SybilWorkspaceView.swift b/ios/Packages/Sybil/Sources/Sybil/SybilWorkspaceView.swift index 504d5ea..7dc1422 100644 --- a/ios/Packages/Sybil/Sources/Sybil/SybilWorkspaceView.swift +++ b/ios/Packages/Sybil/Sources/Sybil/SybilWorkspaceView.swift @@ -50,7 +50,7 @@ struct SybilWorkspaceView: View { } private var showsCustomWorkspaceNavigation: Bool { - usesCustomWorkspaceNavigation && (!isSettingsSelected || navigationLeadingControl == .back) + usesCustomWorkspaceNavigation && (!isSettingsSelected || navigationLeadingControl != .hidden) } private var transcriptScrollContextID: String {