From 3954ff13b7b34d75d87ab03862d9579647621190 Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Tue, 16 Jun 2026 02:00:05 -0700 Subject: [PATCH] Wrap approval detail and size the box to its content MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first pass put the detail in a ScrollView with fixedSize(horizontal:) and a fixed maxHeight, so the command was forced onto one long line that scrolled horizontally and the box always claimed full height — a tall box with a lot of empty space. Replace it with an ApprovalDetailBox (macOS + iOS) that wraps the text and measures its content height via a PreferenceKey, sizing the box to fit and only scrolling vertically once the content exceeds the max height. No more horizontal scroll; short commands sit in a snug box. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Views/ApprovalCardView.swift | 48 ++++++++++++++----- 1 file changed, 37 insertions(+), 11 deletions(-) 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()) + } +}