native macOS codings agent orchestrator
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

perf(shelf): keep terminal out of spine layout animation

onevcat 84f86c79 dfd40af4

+65 -5
+65 -5
supacode/Features/Shelf/Views/ShelfView.swift
··· 37 37 books.firstIndex(where: { $0.id == id }) 38 38 } 39 39 40 + ZStack(alignment: .leading) { 41 + spineFlow(books: books, openBookID: openBookID, openIndex: openIndex) 42 + openBookOverlay(books: books, openIndex: openIndex, state: state) 43 + } 44 + .frame(maxWidth: .infinity, maxHeight: .infinity) 45 + .background(Color(nsColor: .windowBackgroundColor).opacity(surfaceBackgroundOpacity)) 46 + } 47 + 48 + @ViewBuilder 49 + private func spineFlow(books: [ShelfBook], openBookID: ShelfBook.ID?, openIndex: Int?) 50 + -> some View 51 + { 40 52 HStack(spacing: 0) { 41 53 ForEach(Array(books.enumerated()), id: \.element.id) { index, book in 42 54 spine(book: book, index: index, openIndex: openIndex) 43 55 if book.id == openBookID { 44 - openBookArea(for: book, state: state) 45 - .transition(.opacity) 56 + terminalPlaceholder() 46 57 } 47 58 } 48 59 if openBookID == nil { 49 - emptyOpenArea() 60 + terminalPlaceholder() 50 61 } 51 62 } 52 63 .frame(maxWidth: .infinity, maxHeight: .infinity) 53 - .background(Color(nsColor: .windowBackgroundColor).opacity(surfaceBackgroundOpacity)) 54 64 // Animate on every openBookID change — covers both Shelf-originated 55 65 // book switches (which also set their own TCA animation) and 56 66 // left-nav-originated switches, so the spine flow is consistent 57 - // regardless of entry point. 67 + // regardless of entry point. The real terminal is rendered in a 68 + // non-layout overlay, so this animation only moves spines and the 69 + // lightweight placeholder. 58 70 .animation(.easeInOut(duration: 0.2), value: openBookID) 71 + } 72 + 73 + @ViewBuilder 74 + private func terminalPlaceholder() -> some View { 75 + Color.clear 76 + .frame(maxWidth: .infinity, maxHeight: .infinity) 77 + .layoutPriority(1) 78 + .accessibilityHidden(true) 59 79 } 60 80 61 81 @ViewBuilder ··· 85 105 store.send(.repositoryManagement(.openRepositorySettings(book.repositoryID))) 86 106 } 87 107 ) 108 + } 109 + 110 + @ViewBuilder 111 + private func openBookOverlay( 112 + books: [ShelfBook], openIndex: Int?, state: RepositoriesFeature.State 113 + ) -> some View { 114 + GeometryReader { proxy in 115 + let frame = openBookOverlayFrame(in: proxy.size, bookCount: books.count, openIndex: openIndex) 116 + openBookOverlayContent(books: books, openIndex: openIndex, state: state) 117 + .frame(width: frame.width, height: proxy.size.height) 118 + .clipped() 119 + .offset(x: frame.minX) 120 + } 121 + } 122 + 123 + private func openBookOverlayFrame(in size: CGSize, bookCount: Int, openIndex: Int?) -> ( 124 + minX: CGFloat, width: CGFloat 125 + ) { 126 + let leftSpineCount = openIndex.map { $0 + 1 } ?? bookCount 127 + let rightSpineCount = openIndex.map { bookCount - $0 - 1 } ?? 0 128 + let minX = CGFloat(leftSpineCount) * ShelfMetrics.spineWidth 129 + let occupiedWidth = CGFloat(leftSpineCount + rightSpineCount) * ShelfMetrics.spineWidth 130 + return (minX, max(0, size.width - occupiedWidth)) 131 + } 132 + 133 + @ViewBuilder 134 + private func openBookOverlayContent( 135 + books: [ShelfBook], 136 + openIndex: Int?, 137 + state: RepositoriesFeature.State 138 + ) -> some View { 139 + if let openIndex { 140 + openBookArea(for: books[openIndex], state: state) 141 + .id(books[openIndex].id) 142 + .transition(.opacity.animation(.easeInOut(duration: 0.12))) 143 + } else { 144 + emptyOpenArea() 145 + .id("__empty__") 146 + .transition(.opacity.animation(.easeInOut(duration: 0.12))) 147 + } 88 148 } 89 149 90 150 /// Dispatch the open-book action only when `book` isn't already the open