diff --git a/NucleicRemote/NucleicRemote/Views/ApprovalCardView.swift b/NucleicRemote/NucleicRemote/Views/ApprovalCardView.swift index f07b28c..bc65cae 100644 --- a/NucleicRemote/NucleicRemote/Views/ApprovalCardView.swift +++ b/NucleicRemote/NucleicRemote/Views/ApprovalCardView.swift @@ -33,18 +33,11 @@ struct ApprovalCardView: View { .background(approval.risk.color.opacity(0.2), in: Capsule()) .foregroundStyle(approval.risk.color) } - // Full, untruncated content in a bounded scroll view — the user must be - // able to read exactly what they are granting, so long commands/paths/inputs - // scroll instead of being clipped to a few lines. + // Full, untruncated content — the command wraps and the box fits its + // content, only scrolling vertically once it's genuinely tall, so the user + // can read exactly what they are granting without a giant half-empty box. if !approval.input.approvalDetail.isEmpty { - ScrollView([.horizontal, .vertical]) { - Text(approval.input.approvalDetail) - .font(.caption.monospaced()) - .textSelection(.enabled) - .padding(8) - } - .frame(maxWidth: .infinity, maxHeight: 200, alignment: .leading) - .background(Color(.secondarySystemBackground), in: RoundedRectangle(cornerRadius: 8)) + ApprovalDetailBox(text: approval.input.approvalDetail) } if requiresBiometric && !biometricPassed { @@ -118,3 +111,36 @@ struct ApprovalCardView: View { } } } + +/// The full content of an approval, wrapped and selectable. The box sizes to its +/// content and only begins scrolling once the content exceeds `maxHeight`, so a short +/// command sits in a snug box while a large paste stays bounded. The text wraps; it +/// never scrolls horizontally. +private struct ApprovalDetailBox: View { + let text: String + private let maxHeight: CGFloat = 200 + @State private var contentHeight: CGFloat = 0 + + var body: some View { + ScrollView(.vertical) { + Text(text) + .font(.caption.monospaced()) + .textSelection(.enabled) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(8) + .background(GeometryReader { geo in + Color.clear.preference(key: ApprovalDetailHeightKey.self, value: geo.size.height) + }) + } + .frame(height: contentHeight == 0 ? nil : min(contentHeight, maxHeight)) + .onPreferenceChange(ApprovalDetailHeightKey.self) { contentHeight = $0 } + .background(Color(.secondarySystemBackground), in: RoundedRectangle(cornerRadius: 8)) + } +} + +private struct ApprovalDetailHeightKey: PreferenceKey { + static var defaultValue: CGFloat { 0 } + static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { + value = max(value, nextValue()) + } +}