From 08501cb831912e3f9e688c995c23135025db80cd Mon Sep 17 00:00:00 2001 From: Roman <51091564+jeanpierreroma@users.noreply.github.com> Date: Sun, 9 Aug 2026 12:31:59 +0300 Subject: [PATCH 1/3] feat(BottomSheet): let the host pick the sheet background MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fill was hardcoded to `primaryBackground` in five places, and the presentation background — the one that covers the home-indicator inset — was only applied when a `cornerRadius` happened to be passed. A host that wanted anything else had two bad options: restyle its own content and live with a pale strip along the bottom edge, or pass a corner radius it did not want purely for the side effect. `background` is now a parameter on `BottomSheet`, on the `selfSizing` factory and on `selfSizingSheet(…)`, defaulting to what it was. The presentation fill applies on iOS 16.4+ regardless of corner radius, so the strip matches whatever the sheet is filled with. Why the strip exists at all: the natural-height path deliberately measures without the bottom safe area, and `presentationDetents([.height])` adds that inset back — so it lies outside the sheet's own stack and needs the presentation background to be covered. --- .../DashUIKit/Components/BottomSheet.swift | 65 +++++++++++++++---- docs/navigation-and-containers.md | 9 ++- 2 files changed, 61 insertions(+), 13 deletions(-) diff --git a/Sources/DashUIKit/Components/BottomSheet.swift b/Sources/DashUIKit/Components/BottomSheet.swift index 1c96b2b..e5b992d 100644 --- a/Sources/DashUIKit/Components/BottomSheet.swift +++ b/Sources/DashUIKit/Components/BottomSheet.swift @@ -17,6 +17,11 @@ public struct BottomSheet: View { /// when natural sizing is needed — it guarantees `fillsHeight: false` and the modifier are /// always applied together. public var fillsHeight: Bool = true + /// Fill behind the whole sheet — grabber, header and content alike. Also + /// used as the presentation background so the home-indicator inset the + /// detent adds matches; a host that only restyles its own content would + /// otherwise get a strip of this colour along the bottom edge. + public var background: Color = .dash.primaryBackground @ViewBuilder public var content: () -> Content public init( @@ -24,12 +29,14 @@ public struct BottomSheet: View { showBackButton: Binding, onBackButtonPressed: (() -> Void)? = nil, fillsHeight: Bool = true, + background: Color = .dash.primaryBackground, @ViewBuilder content: @escaping () -> Content ) { self.title = title self._showBackButton = showBackButton self.onBackButtonPressed = onBackButtonPressed self.fillsHeight = fillsHeight + self.background = background self.content = content } @@ -42,7 +49,7 @@ public struct BottomSheet: View { contentSection } - .background(Color.dash.primaryBackground) + .background(background) if fillsHeight { sheet.edgesIgnoringSafeArea(.bottom) @@ -103,13 +110,13 @@ public struct BottomSheet: View { .navigationBarHidden(true) #endif .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color.dash.primaryBackground) + .background(background) } } else { // Natural height — no greedy NavigationView / maxHeight so the sheet can self-size. content() .frame(maxWidth: .infinity) - .background(Color.dash.primaryBackground) + .background(background) } } } @@ -134,6 +141,7 @@ public extension BottomSheet { onBackButtonPressed: (() -> Void)? = nil, fallback: CGFloat = 0, maxHeightFraction: CGFloat = 0.95, + background: Color = .dash.primaryBackground, cornerRadius: CGFloat? = nil, @ViewBuilder content: @escaping () -> Content ) -> some View { @@ -142,9 +150,14 @@ public extension BottomSheet { showBackButton: showBackButton, onBackButtonPressed: onBackButtonPressed, fillsHeight: false, + background: background, content: content ) - .selfSizingSheet(fallback: fallback, maxHeightFraction: maxHeightFraction, cornerRadius: cornerRadius) + .selfSizingSheet( + fallback: fallback, + maxHeightFraction: maxHeightFraction, + background: background, + cornerRadius: cornerRadius) } } @@ -163,28 +176,34 @@ public extension View { /// - fallback: Height used before the first measurement (avoids a `.medium` flash). /// - maxHeightFraction: Caps the sheet at this fraction of the window height; taller content /// is clipped, so wrap it in a `ScrollView`. + /// - background: Fill for the sheet and its presentation, so the bottom + /// safe-area strip matches the content. Defaults to the sheet's own. /// - cornerRadius: Optional corner radius applied via `presentationCornerRadius` on - /// iOS 16.4..<26 (iOS 26+ keeps the system corner styling). When provided, the sheet - /// background is also filled so the bottom safe-area strip matches the content. + /// iOS 16.4..<26 (iOS 26+ keeps the system corner styling). @ViewBuilder func selfSizingSheet( fallback: CGFloat = 0, maxHeightFraction: CGFloat = 0.95, + background: Color = .dash.primaryBackground, cornerRadius: CGFloat? = nil ) -> some View { if #available(iOS 16.0, macOS 13.0, *) { let modified = modifier(SelfSizingSheetModifier(fallback: fallback, maxHeightFraction: maxHeightFraction)) #if os(iOS) - if #available(iOS 16.4, *), let cornerRadius { - if #unavailable(iOS 26.0) { - // iOS 16.4..<26: apply the custom corner radius + fill the sheet background. + if #available(iOS 16.4, *) { + // The background is filled whatever the corner radius: the + // measured height excludes the home-indicator inset that + // `.presentationDetents([.height])` adds back, so that strip + // sits outside the sheet's own `VStack` and shows the system + // background unless this fills it. + if #unavailable(iOS 26.0), let cornerRadius { modified .presentationCornerRadius(cornerRadius) - .presentationBackground(Color.dash.primaryBackground) + .presentationBackground(background) } else { - // iOS 26+: keep the system corner styling, just fill the background. + // iOS 26+ keeps the system corner styling. modified - .presentationBackground(Color.dash.primaryBackground) + .presentationBackground(background) } } else { modified @@ -283,3 +302,25 @@ private struct SelfSizingSheetModifier: ViewModifier { .padding() } } + +@available(iOS 17, macOS 14, *) +#Preview("BottomSheet Custom Background") { + BottomSheet( + title: "Bottom Sheet", + showBackButton: .constant(false), + fillsHeight: false, + background: .dash.secondaryBackground + ) { + VStack(alignment: .leading, spacing: 12) { + Text("Cards on a tinted sheet") + .dashFont(.calloutMedium) + .foregroundColor(.dash.primaryText) + + Text("The host picks the fill; cards drawn on top keep their own.") + .dashFont(.body) + .foregroundColor(.dash.secondaryText) + .modifier(MenuViewModifier()) + } + .padding() + } +} diff --git a/docs/navigation-and-containers.md b/docs/navigation-and-containers.md index 6b71640..4d01b03 100644 --- a/docs/navigation-and-containers.md +++ b/docs/navigation-and-containers.md @@ -70,7 +70,8 @@ Sheet chrome to put **inside** a SwiftUI `.sheet { }`: a grabber, a `NavigationB title: "Details", showBackButton: $showBack, // Binding onBackButtonPressed: { /* pop */ }, - fillsHeight: true // greedy: fills the sheet + fillsHeight: true, // greedy: fills the sheet + background: .dash.primaryBackground // fill behind grabber, header and content ) { MyContent() } @@ -94,6 +95,7 @@ the modifier are applied together: showBackButton: .constant(false), fallback: 240, // height before first measurement (avoids .medium flash) maxHeightFraction: 0.95, // cap at 95% of window height (clip taller → use ScrollView) + background: .dash.secondaryBackground, // also fills the home-indicator strip cornerRadius: 24 // iOS 16.4..<26; iOS 26+ keeps system corners ) { MyContent() @@ -107,6 +109,11 @@ a **no-op below iOS 16**. The measured content must have a finite intrinsic heig greedy `Spacer`/`maxHeight: .infinity`), or the measurement is wrong. `BottomSheetHeightPreferenceKey` is exposed for advanced cases. +`background` fills the sheet **and** its presentation. The measured height excludes the +home-indicator inset that `presentationDetents([.height])` adds back, so that strip lies +outside the sheet's own stack — without the presentation fill it shows the system +background as a pale band along the bottom edge, whatever the content is styled with. + --- ## MenuViewModifier From f1fc3cb86427681574995cc16e2398a599572dec Mon Sep 17 00:00:00 2001 From: Roman <51091564+jeanpierreroma@users.noreply.github.com> Date: Sun, 9 Aug 2026 22:29:34 +0300 Subject: [PATCH 2/3] feat(AddressFieldView): let the host add a badge beside the label MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The label row was a bare Text, so a host with something to say about the address it decoded — which chain it turned out to belong to — had nowhere to put it and had to draw its own label row above the field, printing the label twice. An optional accessory sits opposite the label. It is deliberately not inside the field: the controls in there act on the address (paste, scan, clear), while this describes it. Generic over the accessory with an EmptyView convenience initializer, the same shape NavigationBar uses, so existing callers are untouched. Co-Authored-By: Claude Opus 5 --- .../Components/AddressFieldView.swift | 82 +++++++++++++++++-- 1 file changed, 76 insertions(+), 6 deletions(-) diff --git a/Sources/DashUIKit/Components/AddressFieldView.swift b/Sources/DashUIKit/Components/AddressFieldView.swift index 0278960..9cd6707 100644 --- a/Sources/DashUIKit/Components/AddressFieldView.swift +++ b/Sources/DashUIKit/Components/AddressFieldView.swift @@ -18,7 +18,7 @@ import SwiftUI @available(iOS 15, macOS 12, *) -public struct AddressFieldView: View { +public struct AddressFieldView: View { private enum Layout { static let hSpacing: CGFloat = 20 @@ -37,6 +37,11 @@ public struct AddressFieldView: View { private var isDisabled: Bool private var onScanQR: (() -> Void)? private var onPaste: (() -> Void)? + /// Trailing content on the label row — a badge naming what the entered + /// address turned out to be, say. Sits opposite `label`, so it is for + /// something that describes the field rather than acts on it; the + /// controls that act live inside the field itself. + private let accessory: Accessory @FocusState private var isTextFieldFocused: Bool @@ -48,7 +53,8 @@ public struct AddressFieldView: View { errorText: String? = nil, isDisabled: Bool = false, onScanQR: (() -> Void)? = nil, - onPaste: (() -> Void)? = nil + onPaste: (() -> Void)? = nil, + @ViewBuilder accessory: () -> Accessory ) { self._text = text self.label = label @@ -58,14 +64,21 @@ public struct AddressFieldView: View { self.isDisabled = isDisabled self.onScanQR = onScanQR self.onPaste = onPaste + self.accessory = accessory() } public var body: some View { VStack(alignment: .leading, spacing: 10) { - Text(label) - .dashFont(.footnote) - .foregroundStyle(Color.dash.gray500) - .frame(maxWidth: .infinity, alignment: .leading) + HStack(spacing: 8) { + Text(label) + .dashFont(.footnote) + .foregroundStyle(Color.dash.gray500) + + Spacer(minLength: 0) + + accessory + } + .frame(maxWidth: .infinity, alignment: .leading) HStack(alignment: .center, spacing: Layout.hSpacing) { textField @@ -101,6 +114,37 @@ public struct AddressFieldView: View { } } +} + +@available(iOS 15, macOS 12, *) +public extension AddressFieldView where Accessory == EmptyView { + /// No label accessory — the original shape, unchanged for callers that + /// have nothing to put there. + init( + text: Binding, + label: String, + placeholder: String, + hasError: Bool, + errorText: String? = nil, + isDisabled: Bool = false, + onScanQR: (() -> Void)? = nil, + onPaste: (() -> Void)? = nil + ) { + self.init( + text: text, + label: label, + placeholder: placeholder, + hasError: hasError, + errorText: errorText, + isDisabled: isDisabled, + onScanQR: onScanQR, + onPaste: onPaste, + accessory: { EmptyView() }) + } +} + +@available(iOS 15, macOS 12, *) +extension AddressFieldView { // MARK: - Subviews private var showsPasteButton: Bool { @@ -292,5 +336,31 @@ public struct AddressFieldView: View { .padding() } +@available(iOS 17, macOS 14, *) +#Preview("Label accessory") { + AddressFieldView( + text: .constant("yV1D1ivvSUyKPJnbFmzSTVh1MyZ3JbeVkY"), + label: "Address", + placeholder: "Dash address", + hasError: false + ) { + // What the host puts here is its own: a badge naming the kind of + // address that was entered, decided by the host's own decoder. + HStack(spacing: 4) { + Image(systemName: "d.circle.fill") + .font(.system(size: 10, weight: .semibold)) + Text("Transparent address") + .dashFont(.caption2) + } + .foregroundStyle(Color.dash.blueText) + .padding(.horizontal, 8) + .padding(.vertical, 3) + .background(Color.dash.blueAlpha10) + .clipShape(Capsule()) + } + .padding() + .background(Color.dash.primaryBackground) +} + #endif #endif // canImport(UIKit) From d8dff738dd364faf65baa81ae2565e802923e340 Mon Sep 17 00:00:00 2001 From: Roman <51091564+jeanpierreroma@users.noreply.github.com> Date: Sun, 9 Aug 2026 22:33:31 +0300 Subject: [PATCH 3/3] fix(AddressFieldView): move Layout out of the now-generic view A generic type cannot hold static stored properties, so the nested Layout enum stopped compiling the moment the view gained its accessory parameter. SwiftPM accepted it; Xcode did not, which is where it surfaced. Co-Authored-By: Claude Opus 5 --- .../Components/AddressFieldView.swift | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/Sources/DashUIKit/Components/AddressFieldView.swift b/Sources/DashUIKit/Components/AddressFieldView.swift index 9cd6707..4e55a32 100644 --- a/Sources/DashUIKit/Components/AddressFieldView.swift +++ b/Sources/DashUIKit/Components/AddressFieldView.swift @@ -17,18 +17,20 @@ #if canImport(UIKit) import SwiftUI +/// Outside the view, not nested in it: `AddressFieldView` is generic over its +/// accessory, and a generic type cannot hold static stored properties. +private enum Layout { + static let hSpacing: CGFloat = 20 + static let lPadding: CGFloat = 20 + static let tPadding: CGFloat = 10 + static let iconSize: CGFloat = 17 + static let cornerRadius: CGFloat = 16 + static let actionTapArea: CGFloat = 40 +} + @available(iOS 15, macOS 12, *) public struct AddressFieldView: View { - private enum Layout { - static let hSpacing: CGFloat = 20 - static let lPadding: CGFloat = 20 - static let tPadding: CGFloat = 10 - static let iconSize: CGFloat = 17 - static let cornerRadius: CGFloat = 16 - static let actionTapArea: CGFloat = 40 - } - @Binding private var text: String private let label: String private let placeholder: String