From 97584ea37af7f6e5a5fa247aa78bf4af58ad2ec6 Mon Sep 17 00:00:00 2001 From: Roman <51091564+jeanpierreroma@users.noreply.github.com> Date: Wed, 29 Jul 2026 15:21:19 +0300 Subject: [PATCH 1/6] feat(Switch): add SwitchView with on/off and disabled states Implements the switch from the iOS design system (node 1841:259) as a standalone SwiftUI view: a 64x28 track with a 39x24 pill thumb inset by 2, sliding 21pt between states with a 0.2s ease-in-out. Covers all four states. Disabled is read from the isEnabled environment value rather than a parameter, so .disabled(true) on any ancestor works: on + enabled blue on + disabled blueAlpha50 off + enabled gray300Alpha50 off + disabled black1000Alpha20 The off and on colors map exactly to the switch/track-fill-off and switch/track-fill-on design variables. The design system has no track-fill-on-disabled variable, so blueAlpha50 is a stand-in and needs confirming with the designer. --- .../Switch/Components/ThumbView.swift | 37 +++++++++ .../Components/Switch/SwitchView.swift | 77 +++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 Sources/DashUIKit/Components/Switch/Components/ThumbView.swift create mode 100644 Sources/DashUIKit/Components/Switch/SwitchView.swift diff --git a/Sources/DashUIKit/Components/Switch/Components/ThumbView.swift b/Sources/DashUIKit/Components/Switch/Components/ThumbView.swift new file mode 100644 index 0000000..fe58a4d --- /dev/null +++ b/Sources/DashUIKit/Components/Switch/Components/ThumbView.swift @@ -0,0 +1,37 @@ +// +// ThumbView.swift +// DashUIKit +// +// Created by Roman Chornyi on 29.07.2026. +// + +import SwiftUI + +@available(iOS 14, macOS 11, *) +struct ThumbView: View { + + private struct Constants { + static let switchThumbFill: Color = Color.dash.white + static let switchThumbRadius: CGFloat = 1000 + } + + var body: some View { + Rectangle() + .foregroundColor(.clear) + .frame(width: 39, height: 24) + .background(Constants.switchThumbFill) + .cornerRadius(Constants.switchThumbRadius) + .shadow(color: Color(red: 0.1, green: 0.13, blue: 0.15).opacity(0.06), radius: 1, x: 0, y: 1) + .shadow(color: Color(red: 0.1, green: 0.13, blue: 0.15).opacity(0.1), radius: 1.5, x: 0, y: 1) + } +} + +#if DEBUG + +@available(iOS 17, macOS 14, *) +#Preview { + ThumbView() + .padding() +} + +#endif diff --git a/Sources/DashUIKit/Components/Switch/SwitchView.swift b/Sources/DashUIKit/Components/Switch/SwitchView.swift new file mode 100644 index 0000000..54ac3fa --- /dev/null +++ b/Sources/DashUIKit/Components/Switch/SwitchView.swift @@ -0,0 +1,77 @@ +// +// SwitchView.swift +// DashUIKit +// +// Created by Roman Chornyi on 29.07.2026. +// + +import SwiftUI + +@available(iOS 14, macOS 11, *) +public struct SwitchView: View { + + private struct Constants { + static let switchTrackFillOn: Color = Color.dash.blue + static let switchTrackFillOnDisabled: Color = Color.dash.blueAlpha50 + static let switchTrackFillOff: Color = Color.dash.gray300Alpha50 + static let switchTrackFillOffDisabled: Color = Color.dash.black1000Alpha20 + static let switchTrackWidth: CGFloat = 64 + static let switchTrackPadding: CGFloat = 2 + static let switchTrackRadius: CGFloat = 1000 + static let animationDuration: Double = 0.2 + } + + @Environment(\.isEnabled) private var isEnabled + + @Binding private var isOn: Bool + + public init(isOn: Binding) { + self._isOn = isOn + } + + public var body: some View { + HStack(alignment: .center, spacing: 0) { + ThumbView() + } + .frame(maxWidth: .infinity, alignment: isOn ? .trailing : .leading) + .padding(Constants.switchTrackPadding) + .frame(width: Constants.switchTrackWidth, alignment: .leading) + .background(trackFill) + .cornerRadius(Constants.switchTrackRadius) + .animation(.easeInOut(duration: Constants.animationDuration), value: isOn) + .contentShape(Rectangle()) + .onTapGesture { isOn.toggle() } + .accessibilityAddTraits(.isButton) + } + + private var trackFill: Color { + switch (isOn, isEnabled) { + case (true, true): return Constants.switchTrackFillOn + case (true, false): return Constants.switchTrackFillOnDisabled + case (false, true): return Constants.switchTrackFillOff + case (false, false): return Constants.switchTrackFillOffDisabled + } + } +} + +#if DEBUG + +@available(iOS 17, macOS 14, *) +#Preview("States") { + VStack(alignment: .leading, spacing: 20) { + SwitchView(isOn: .constant(true)) + SwitchView(isOn: .constant(false)) + SwitchView(isOn: .constant(true)).disabled(true) + SwitchView(isOn: .constant(false)).disabled(true) + } + .padding() +} + +@available(iOS 17, macOS 14, *) +#Preview("Interactive") { + @Previewable @State var isOn = false + SwitchView(isOn: $isOn) + .padding() +} + +#endif From 43dcbad8b462c846b30de6919c4380d864319d82 Mon Sep 17 00:00:00 2001 From: Roman <51091564+jeanpierreroma@users.noreply.github.com> Date: Wed, 29 Jul 2026 15:48:44 +0300 Subject: [PATCH 2/6] feat(Foundation): add DashIcon enums for every catalog asset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces DashIcon, a namespace of String-backed enums mirroring the asset catalog folder structure — 137 icons across 12 groups. The raw value is the imageset name, so a wrong name becomes a compile error instead of a blank image at runtime. DashIconAsset gives each case .source (a DashIconSource, for components that take one) and .image (a plain Image, styled by the caller). Group prefixes are dropped from case names since the enum already namespaces them: menu-account-error reads as DashIcon.Menu.accountError. Generated from the catalog rather than hand-written, and CaseIterable so a gallery preview can enumerate a group. --- .../DashUIKit/Foundation/Icon_DashUI.swift | 247 ++++++++++++++++++ 1 file changed, 247 insertions(+) create mode 100644 Sources/DashUIKit/Foundation/Icon_DashUI.swift diff --git a/Sources/DashUIKit/Foundation/Icon_DashUI.swift b/Sources/DashUIKit/Foundation/Icon_DashUI.swift new file mode 100644 index 0000000..6c43553 --- /dev/null +++ b/Sources/DashUIKit/Foundation/Icon_DashUI.swift @@ -0,0 +1,247 @@ +// +// Created by Roman Chornyi +// Copyright © 2026 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +/// A named icon shipped in DashUIKit's asset catalog. +/// +/// Every icon group below is a `String`-backed enum whose raw value is the +/// imageset name, so a typo becomes a compile error instead of a blank image. +/// +/// ```swift +/// Image(dash: DashIcon.Menu.send.source) +/// DashIcon.Toast.success.image.resizable().frame(width: 24, height: 24) +/// ``` +@available(iOS 14, macOS 11, *) +public protocol DashIconAsset { + /// The imageset name in the asset catalog. + var assetName: String { get } +} + +@available(iOS 14, macOS 11, *) +public extension DashIconAsset where Self: RawRepresentable, Self.RawValue == String { + var assetName: String { rawValue } + + /// The icon as a `DashIconSource`, for components that take one. + var source: DashIconSource { .custom(assetName, bundle: .dashUIKit) } + + /// The icon as a plain `Image`. Styling is the caller's job. + var image: Image { Image(dash: source) } +} + +/// Namespace for every icon in the library's asset catalog, grouped the same +/// way the catalog is. +@available(iOS 14, macOS 11, *) +public enum DashIcon { + + // MARK: - Common + /// `root of the catalog` + public enum Common: String, CaseIterable, DashIconAsset { + case arrowDown = "arrow-down" + case checkmark = "checkmark" + case chevronDownCurrencySelect = "chevron-down-currency-select" + case diagonalUpDown = "diagonal-up-down" + case enterAmountDash = "enter-amount-dash" + case iconDashCurrency = "icon_dash_currency" + case illustrationXmark = "illustration-xmark" + } + + // MARK: - Icons + /// `Icons` + public enum Icons: String, CaseIterable, DashIconAsset { + case copyOutline = "copy-outline" + } + + // MARK: - Illustrations + /// `Illustrations` + public enum Illustrations: String, CaseIterable, DashIconAsset { + case crowdnodeWarning = "crowdnode.warning" + case dashDex = "illustration-dash-dex" + } + + // MARK: - Checkbox + /// `Components/Checkbox` + public enum Checkbox: String, CaseIterable, DashIconAsset { + case checkmarkChecked = "checkbox-checkmark-checked" + case checkmarkUnchecked = "checkbox-checkmark-unchecked" + } + + // MARK: - SearchBar + /// `Components/SearchBar` + public enum SearchBar: String, CaseIterable, DashIconAsset { + case magnifyingglassIcon = "searchbar-magnifyingglass-icon" + case xmarkIcon = "searchbar-xmark-icon" + } + + // MARK: - Menu + /// `Menu` + public enum Menu: String, CaseIterable, DashIconAsset { + case accountError = "menu-account-error" + case accountLink = "menu-account-link" + case accountOnline = "menu-account-online" + case accountProcessing = "menu-account-processing" + case accountSuccess = "menu-account-success" + case accountWarning = "menu-account-warning" + case addressBook = "menu-address-book" + case advancedSecurity = "menu-advanced-security" + case appReview = "menu-app-review" + case appearance = "menu-appearance" + case applePay = "menu-apple-pay" + case atm = "menu-atm" + case autohideBalance = "menu-autohide-balance" + case backup = "menu-backup" + case bank = "menu-bank" + case battery = "menu-battery" + case blockchair = "menu-blockchair" + case buySell = "menu-buy-sell" + case buySellDash2 = "menu-buy-sell-dash-2" + case clipboard = "menu-clipboard" + case coinbase = "menu-coinbase" + case connections = "menu-connections" + case convert = "menu-convert" + case creditCard = "menu-credit-card" + case credits = "menu-credits" + case crowdnode = "menu-crowdnode" + case csvExport = "menu-csv-export" + case ctx = "menu-ctx" + case currentLocation = "menu-current-location" + case dashLogoSquare = "menu-dash-logo-square" + case explore = "menu-explore" + case extendPublicKey = "menu-extend-public-key" + case faceID = "menu-face-id" + case faceIDRounded = "menu-face-id-rounded" + case file = "menu-file" + case flexa = "menu-flexa" + case floppyDisk = "menu-floppy-disk" + case gPay = "menu-g-pay" + case importPrivateKey = "menu-import-private-key" + case infoRect = "menu-info-rect" + case invitation = "menu-invitation" + case invitationOpen = "menu-invitation-open" + case linkAccount2 = "menu-link-account-2" + case localCurrency = "menu-local-currency" + case logout = "menu-logout" + case masternodeKeys = "menu-masternode-keys" + case maya = "menu-maya" + case merchant = "menu-merchant" + case mixing = "menu-mixing" + case networkMonitor = "menu-network-monitor" + case notification = "menu-notification" + case notificationNew = "menu-notification-new" + case paypal = "menu-paypal" + case piggyCards = "menu-piggy-cards" + case pin = "menu-pin" + case qr = "menu-qr" + case receive = "menu-receive" + case receiveDisabled = "menu-receive-disabled" + case recoveryPhrase = "menu-recovery-phrase" + case rescanBlockchain = "menu-rescan-blockchain" + case resetWallet = "menu-reset-wallet" + case reverseSyncing = "menu-reverse-syncing" + case scanQR = "menu-scan-qr" + case security = "menu-security" + case send = "menu-send" + case sendAccount = "menu-send-account" + case sendAddress = "menu-send-address" + case sendDisabled = "menu-send-disabled" + case settings = "menu-settings" + case shield = "menu-shield" + case shieldAdvanced = "menu-shield-advanced" + case shieldIntermediate = "menu-shield-intermediate" + case shortcuts = "menu-shortcuts" + case spend = "menu-spend" + case spendingConfirmation = "menu-spending-confirmation" + case staking = "menu-staking" + case support = "menu-support" + case tools = "menu-tools" + case tools2 = "menu-tools-2" + case topper = "menu-topper" + case touchID = "menu-touch-id" + case transfer = "menu-transfer" + case uphold = "menu-uphold" + case userSearch = "menu-user-search" + case usernameVoting = "menu-username-voting" + case wallet = "menu-wallet" + case zenledger = "menu-zenledger" + } + + // MARK: - NavigationBar + /// `Navigation bar` + public enum NavigationBar: String, CaseIterable, DashIconAsset { + case back = "navigationbar-back" + case close = "navigationbar-close" + case info = "navigationbar-info" + case plus = "navigationbar-plus" + } + + // MARK: - SystemMessage + /// `System message` + public enum SystemMessage: String, CaseIterable, DashIconAsset { + case infoRectSmall = "system-message-info-rect-small" + case shieldSmall = "system-message-shield-small" + case timerSmall = "system-message-timer-small" + case unmixedFunds = "system-message-unmixed-funds" + case usernameChange = "system-message-username-change" + case warningTriangle = "system-message-warning-triangle" + } + + // MARK: - Toast + /// `Toast` + public enum Toast: String, CaseIterable, DashIconAsset { + case copied = "toast-copied" + case error = "toast-error" + case info = "toast-info" + case noWifi = "toast-no-wifi" + case success = "toast-success" + case warning = "toast-warning" + } + + // MARK: - Other + /// `Other` + public enum Other: String, CaseIterable, DashIconAsset { + case textFieldClear = "text-field-clear" + case textFieldQR = "text-field-qr" + } + + // MARK: - AdditionalInfo + /// `Transactions/AdditionalInfo` + public enum AdditionalInfo: String, CaseIterable, DashIconAsset { + case error = "additional-info-error" + case giftCard = "additional-info-gift-card" + case received = "additional-info-received" + case sent = "additional-info-sent" + } + + // MARK: - Transaction + /// `Transactions/Preview` + public enum Transaction: String, CaseIterable, DashIconAsset { + case allTrans = "transaction-all-trans" + case coinbaseReceived = "transaction-coinbase-received" + case contactRequestApprove = "transaction-contact-request-approve" + case contactRequestSent = "transaction-contact-request-sent" + case convert = "transaction-convert" + case crowdnode = "transaction-crowdnode" + case error = "transaction-error" + case giftCard = "transaction-gift-card" + case internalTransfer = "transaction-internal-transfer" + case mining = "transaction-mining" + case mixing = "transaction-mixing" + case received = "transaction-received" + case sent = "transaction-sent" + case upholdReceived = "transaction-uphold-received" + } +} From 407de622cabff6042f1beab221455a5e0e569705 Mon Sep 17 00:00:00 2001 From: Roman <51091564+jeanpierreroma@users.noreply.github.com> Date: Wed, 29 Jul 2026 15:50:14 +0300 Subject: [PATCH 3/6] refactor: reference icons through DashIcon instead of raw names Replaces all 20 raw asset-name strings in the library with DashIcon cases. Three of them were already broken by the catalog normalization and were failing silently: - SystemMessageView's default icon still pointed at "warning_triangle", renamed to system-message-warning-triangle. This is a default parameter value in shipping code, not a preview, so every SystemMessageView built without an explicit icon rendered a blank one. - MenuItem's preview used "menu-receive.disabled"; the imageset is now menu-receive-disabled. - TransactionView passed "transaction-mining" with no bundle, so it resolved against the host app's bundle rather than the library's. Call sites that only needed an Image collapse from Image(dash: .custom(name, bundle: .dashUIKit)) to DashIcon.Group.case.image. --- Sources/DashUIKit/Components/AddressFieldView.swift | 4 ++-- Sources/DashUIKit/Components/DashAmount.swift | 2 +- .../Components/EnterAmount/DashBalanceView.swift | 2 +- .../Components/EnterAmount/SwapAmountView.swift | 10 +++++----- .../Components/Illustrations/ErrorIllustration.swift | 2 +- .../Components/Illustrations/SuccessIllustration.swift | 2 +- Sources/DashUIKit/Components/MenuItem.swift | 6 +++--- Sources/DashUIKit/Components/SearchBar.swift | 8 ++++---- Sources/DashUIKit/Components/SystemMessageView.swift | 2 +- .../Components/Transaction/TransactionView.swift | 2 +- 10 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Sources/DashUIKit/Components/AddressFieldView.swift b/Sources/DashUIKit/Components/AddressFieldView.swift index 7c231b6..dde2cd1 100644 --- a/Sources/DashUIKit/Components/AddressFieldView.swift +++ b/Sources/DashUIKit/Components/AddressFieldView.swift @@ -150,7 +150,7 @@ public struct AddressFieldView: View { Group { if text.isEmpty { Button(action: { onScanQR?() }) { - Image(dash: .custom("text-field-qr", bundle: .dashUIKit)) + DashIcon.Other.textFieldQR.image .resizable() .scaledToFit() .frame(width: Layout.iconSize, height: Layout.iconSize) @@ -158,7 +158,7 @@ public struct AddressFieldView: View { .accessibilityLabel(NSLocalizedString("Scan QR code", bundle: .module, comment: "DashUIKit")) } else { Button(action: { text = "" }) { - Image(dash: .custom("text-field-clear", bundle: .dashUIKit)) + DashIcon.Other.textFieldClear.image .renderingMode(.template) .resizable() .scaledToFit() diff --git a/Sources/DashUIKit/Components/DashAmount.swift b/Sources/DashUIKit/Components/DashAmount.swift index 7824df3..115145a 100644 --- a/Sources/DashUIKit/Components/DashAmount.swift +++ b/Sources/DashUIKit/Components/DashAmount.swift @@ -88,7 +88,7 @@ public struct DashAmount: View { Text(DashAmountFormat.string(forDuffs: abs(amount))) .font(.system(size: fontSize, weight: weight)) .lineLimit(1) - Image(dash: .custom("icon_dash_currency", bundle: .dashUIKit)) + DashIcon.Common.iconDashCurrency.image .resizable() .aspectRatio(contentMode: .fit) .frame(width: fontSize * dashSymbolFactor, diff --git a/Sources/DashUIKit/Components/EnterAmount/DashBalanceView.swift b/Sources/DashUIKit/Components/EnterAmount/DashBalanceView.swift index b8243ec..baac368 100644 --- a/Sources/DashUIKit/Components/EnterAmount/DashBalanceView.swift +++ b/Sources/DashUIKit/Components/EnterAmount/DashBalanceView.swift @@ -54,7 +54,7 @@ public struct DashBalanceView: View { } private var dashSymbol: some View { - Image(dash: .custom("icon_dash_currency", bundle: .dashUIKit)) + DashIcon.Common.iconDashCurrency.image .resizable() .aspectRatio(contentMode: .fit) .frame(width: 12, height: 10) diff --git a/Sources/DashUIKit/Components/EnterAmount/SwapAmountView.swift b/Sources/DashUIKit/Components/EnterAmount/SwapAmountView.swift index 38562fc..75293b0 100644 --- a/Sources/DashUIKit/Components/EnterAmount/SwapAmountView.swift +++ b/Sources/DashUIKit/Components/EnterAmount/SwapAmountView.swift @@ -166,7 +166,7 @@ public struct SwapAmountView: View { Button { onCurrencyTap?() } label: { - Image(dash: .custom("chevron-down-currency-select", bundle: .dashUIKit)) + DashIcon.Common.chevronDownCurrencySelect.image .frame(width: 10, height: 5) } .buttonStyle(.plain) @@ -191,7 +191,7 @@ public struct SwapAmountView: View { .font(font) if showDashLogo { - Image(dash: .custom("enter-amount-dash", bundle: .dashUIKit)) + DashIcon.Common.enterAmountDash.image .resizable() .scaledToFit() .frame(width: dashSize?.width, height: dashSize?.height) @@ -210,7 +210,7 @@ public struct SwapAmountView: View { .font(font) if showSecondaryDashLogo { - Image(dash: .custom("enter-amount-dash", bundle: .dashUIKit)) + DashIcon.Common.enterAmountDash.image .resizable() .scaledToFit() .frame(width: dashSize?.width, height: dashSize?.height) @@ -365,7 +365,7 @@ private struct AnimatedSwapLayout: View { if showSecondaryCurrencyButton { Button { onSecondaryCurrencyTap?() } label: { - Image(dash: .custom("chevron-down-currency-select", bundle: .dashUIKit)) + DashIcon.Common.chevronDownCurrencySelect.image .resizable() .scaledToFit() .frame(width: chevronSize.width, height: chevronSize.height) @@ -436,7 +436,7 @@ private struct AnimatedSwapLayout: View { } Text(displayText).font(font) if showLogo { - Image(dash: .custom("enter-amount-dash", bundle: .dashUIKit)) + DashIcon.Common.enterAmountDash.image .resizable() .scaledToFit() .frame(width: dashSize.width, height: dashSize.height) diff --git a/Sources/DashUIKit/Components/Illustrations/ErrorIllustration.swift b/Sources/DashUIKit/Components/Illustrations/ErrorIllustration.swift index df39527..2265e0f 100644 --- a/Sources/DashUIKit/Components/Illustrations/ErrorIllustration.swift +++ b/Sources/DashUIKit/Components/Illustrations/ErrorIllustration.swift @@ -26,7 +26,7 @@ public struct ErrorIllustration: View { ZStack { Color.dash.red - Image(dash: .custom("illustration-xmark", bundle: .dashUIKit)) + DashIcon.Common.illustrationXmark.image .resizable() .scaledToFit() .frame(maxHeight: 29) diff --git a/Sources/DashUIKit/Components/Illustrations/SuccessIllustration.swift b/Sources/DashUIKit/Components/Illustrations/SuccessIllustration.swift index 6f9631e..f1dc914 100644 --- a/Sources/DashUIKit/Components/Illustrations/SuccessIllustration.swift +++ b/Sources/DashUIKit/Components/Illustrations/SuccessIllustration.swift @@ -26,7 +26,7 @@ public struct SuccessIllustration: View { ZStack { Color.dash.green - Image(dash: .custom("checkmark", bundle: .dashUIKit)) + DashIcon.Common.checkmark.image .resizable() .scaledToFit() .frame(maxHeight: 29) diff --git a/Sources/DashUIKit/Components/MenuItem.swift b/Sources/DashUIKit/Components/MenuItem.swift index fd7e6c0..10e9b26 100644 --- a/Sources/DashUIKit/Components/MenuItem.swift +++ b/Sources/DashUIKit/Components/MenuItem.swift @@ -249,7 +249,7 @@ public struct MenuItem: View { #Preview("Enabled vs disabled") { VStack(spacing: 0) { MenuItem( - leadingIcon: .custom("menu-receive", bundle: .dashUIKit), + leadingIcon: DashIcon.Menu.receive.source, title: "Buy Dash", helpText: "From any crypto to your Dash Wallet" ) @@ -257,9 +257,9 @@ public struct MenuItem: View { Divider().padding(.leading, 16) MenuItem( - leadingIcon: .custom("menu-receive", bundle: .dashUIKit), + leadingIcon: DashIcon.Menu.receive.source, isEnabled: false, - disabledLeadingIcon: .custom("menu-receive.disabled", bundle: .dashUIKit), + disabledLeadingIcon: DashIcon.Menu.receiveDisabled.source, title: "Buy Dash", helpText: "From any crypto to your Dash Wallet" ) diff --git a/Sources/DashUIKit/Components/SearchBar.swift b/Sources/DashUIKit/Components/SearchBar.swift index 1cad5c2..603c8e6 100644 --- a/Sources/DashUIKit/Components/SearchBar.swift +++ b/Sources/DashUIKit/Components/SearchBar.swift @@ -93,7 +93,7 @@ private struct SearchBarFocused: View { } private var magnifyingGlass: some View { - Image(dash: .custom("searchbar-magnifyingglass-icon", bundle: .dashUIKit)) + DashIcon.SearchBar.magnifyingglassIcon.image .resizable() .scaledToFit() .frame(maxHeight: 15) @@ -105,7 +105,7 @@ private struct SearchBarFocused: View { Button( action: { text = "" }, label: { - Image(dash: .custom("searchbar-xmark-icon", bundle: .dashUIKit)) + DashIcon.SearchBar.xmarkIcon.image .resizable() .scaledToFit() .frame(maxHeight: 15) @@ -188,7 +188,7 @@ private struct SearchBarLegacy: View { } private var magnifyingGlass: some View { - Image(dash: .custom("searchbar-magnifyingglass-icon", bundle: .dashUIKit)) + DashIcon.SearchBar.magnifyingglassIcon.image .resizable() .scaledToFit() .frame(maxHeight: 15) @@ -200,7 +200,7 @@ private struct SearchBarLegacy: View { Button( action: { text = "" }, label: { - Image(dash: .custom("searchbar-xmark-icon", bundle: .dashUIKit)) + DashIcon.SearchBar.xmarkIcon.image .resizable() .scaledToFit() .frame(maxHeight: 15) diff --git a/Sources/DashUIKit/Components/SystemMessageView.swift b/Sources/DashUIKit/Components/SystemMessageView.swift index e92d8bc..67752c8 100644 --- a/Sources/DashUIKit/Components/SystemMessageView.swift +++ b/Sources/DashUIKit/Components/SystemMessageView.swift @@ -32,7 +32,7 @@ public struct SystemMessageView: View { public init( title: String, subtitle: String? = nil, - icon: DashIconSource = .custom("warning_triangle", bundle: .dashUIKit), + icon: DashIconSource = DashIcon.SystemMessage.warningTriangle.source, backgroundColor: Color = Color.dash.gray300Alpha10, buttonName: String? = nil, onAction: (() -> Void)? = nil, diff --git a/Sources/DashUIKit/Components/Transaction/TransactionView.swift b/Sources/DashUIKit/Components/Transaction/TransactionView.swift index e10c035..9050d45 100644 --- a/Sources/DashUIKit/Components/Transaction/TransactionView.swift +++ b/Sources/DashUIKit/Components/Transaction/TransactionView.swift @@ -273,7 +273,7 @@ public struct TransactionView: View { @available(iOS 17, macOS 14, *) #Preview("Locked reward") { TransactionView( - icon: .custom("transaction-mining"), + icon: DashIcon.Transaction.mining.source, title: "Reward", subtitle: "8:34 AM", dashAmount: 250_000_000, From 266d64319377c2c2858b1ffaa379fd916aab9b86 Mon Sep 17 00:00:00 2001 From: Roman <51091564+jeanpierreroma@users.noreply.github.com> Date: Wed, 29 Jul 2026 15:51:05 +0300 Subject: [PATCH 4/6] refactor: apply text styles with dashFont instead of font MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Converts every unambiguous .font(Font.dash.X) to .dashFont(.X), so the documented design line height is applied alongside the font rather than leaving each Text at the system's natural leading. Also maps two preview labels off Apple's .subheadline onto .dashFont(.subhead). Deliberately left on .font: - The previews in LineHeight+DashUI and DashTextStyle exist precisely to contrast bare .font against .dashFont — converting them would erase what they demonstrate. - SearchBar's Font.dash.footnote.weight(.semibold): the nearest token, footnoteMedium, is .medium, so converting would change the weight. - DashButtonSize.fontSize returns .system(size: 16/14/13/12); 14pt has no token, so a partial conversion would leave the sizes inconsistent. - DashAmount, NumericKeyboardView and SwapAmountView size text dynamically or take a Font parameter. Converting those means reshaping the API to pass DashTextStyle, which is a separate change. --- Sources/DashUIKit/Button/DashButton.swift | 4 ++-- Sources/DashUIKit/Components/AddressFieldView.swift | 6 +++--- .../Components/EnterAmount/DashBalanceView.swift | 4 ++-- .../Components/EnterAmount/DashPickerView.swift | 2 +- .../Components/EnterAmount/DualSwapAmountView.swift | 2 +- .../Components/EnterAmount/EnterAmountView.swift | 6 +++--- .../Components/EnterAmount/SwapAmountView.swift | 10 +++++----- Sources/DashUIKit/Components/NumericKeyboardView.swift | 2 +- Sources/DashUIKit/Components/SearchBar.swift | 2 +- Sources/DashUIKit/Foundation/LineHeight+DashUI.swift | 4 ++-- 10 files changed, 21 insertions(+), 21 deletions(-) diff --git a/Sources/DashUIKit/Button/DashButton.swift b/Sources/DashUIKit/Button/DashButton.swift index 15ed94a..c84b256 100644 --- a/Sources/DashUIKit/Button/DashButton.swift +++ b/Sources/DashUIKit/Button/DashButton.swift @@ -273,7 +273,7 @@ private struct DashButtonStylePreview: View { VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 8) { Text("Enabled") - .font(.subheadline) + .dashFont(.subhead) .foregroundColor(.secondary) HStack(alignment: .top, spacing: 12) { @@ -291,7 +291,7 @@ private struct DashButtonStylePreview: View { VStack(alignment: .leading, spacing: 8) { Text("Disabled") - .font(.subheadline) + .dashFont(.subhead) .foregroundColor(.secondary) HStack(alignment: .top, spacing: 12) { diff --git a/Sources/DashUIKit/Components/AddressFieldView.swift b/Sources/DashUIKit/Components/AddressFieldView.swift index dde2cd1..db53e1a 100644 --- a/Sources/DashUIKit/Components/AddressFieldView.swift +++ b/Sources/DashUIKit/Components/AddressFieldView.swift @@ -122,12 +122,12 @@ public struct AddressFieldView: View { "", text: $text, prompt: Text(placeholder) - .font(Font.dash.subhead) + .dashFont(.subhead) .foregroundStyle(Color.dash.black1000Alpha30), axis: .vertical ) .lineLimit(1...2) - .font(Font.dash.subhead) + .dashFont(.subhead) .textInputAutocapitalization(.never) .disableAutocorrection(true) .foregroundStyle(Color.dash.primaryText) @@ -136,7 +136,7 @@ public struct AddressFieldView: View { .disabled(isDisabled) } else { TextField(placeholder, text: $text) - .font(Font.dash.subhead) + .dashFont(.subhead) .textInputAutocapitalization(.never) .disableAutocorrection(true) .foregroundStyle(Color.dash.primaryText) diff --git a/Sources/DashUIKit/Components/EnterAmount/DashBalanceView.swift b/Sources/DashUIKit/Components/EnterAmount/DashBalanceView.swift index baac368..20e5264 100644 --- a/Sources/DashUIKit/Components/EnterAmount/DashBalanceView.swift +++ b/Sources/DashUIKit/Components/EnterAmount/DashBalanceView.swift @@ -38,7 +38,7 @@ public struct DashBalanceView: View { VStack(alignment: .trailing, spacing: 0) { HStack(spacing: 6) { Text(balance) - .font(Font.dash.subhead) + .dashFont(.subhead) .lineLimit(1) dashSymbol @@ -47,7 +47,7 @@ public struct DashBalanceView: View { if let fiat { Text(fiat) - .font(Font.dash.caption1) + .dashFont(.caption1) .foregroundColor(Color.dash.primaryText) } } diff --git a/Sources/DashUIKit/Components/EnterAmount/DashPickerView.swift b/Sources/DashUIKit/Components/EnterAmount/DashPickerView.swift index e9817ae..787e7da 100644 --- a/Sources/DashUIKit/Components/EnterAmount/DashPickerView.swift +++ b/Sources/DashUIKit/Components/EnterAmount/DashPickerView.swift @@ -52,7 +52,7 @@ public struct DashPickerView: View { private func pickerOption(_ option: Option) -> some View { Text(title(option)) - .font(Font.dash.caption2) + .dashFont(.caption2) .foregroundColor(selected == option ? Color.dash.primaryText : Color.dash.tertiaryText) .padding(.horizontal, Layout.hPadding) .padding(.vertical, Layout.vPadding) diff --git a/Sources/DashUIKit/Components/EnterAmount/DualSwapAmountView.swift b/Sources/DashUIKit/Components/EnterAmount/DualSwapAmountView.swift index 7ee6301..fce30a6 100644 --- a/Sources/DashUIKit/Components/EnterAmount/DualSwapAmountView.swift +++ b/Sources/DashUIKit/Components/EnterAmount/DualSwapAmountView.swift @@ -81,7 +81,7 @@ internal struct DualInputTypeSwitcher: View { onSelect(code) } label: { Text(code) - .font(Font.dash.caption2) + .dashFont(.caption2) .foregroundColor( code == selected ? Color.dash.primaryText diff --git a/Sources/DashUIKit/Components/EnterAmount/EnterAmountView.swift b/Sources/DashUIKit/Components/EnterAmount/EnterAmountView.swift index f0c406d..ff92126 100644 --- a/Sources/DashUIKit/Components/EnterAmount/EnterAmountView.swift +++ b/Sources/DashUIKit/Components/EnterAmount/EnterAmountView.swift @@ -228,7 +228,7 @@ public struct EnterAmountView: View { .frame(width: 40, height: 40) Text(NSLocalizedString("Max", bundle: .module, comment: "")) - .font(Font.dash.caption2) + .dashFont(.caption2) .foregroundColor(Color.dash.blue) } } @@ -339,7 +339,7 @@ private struct DualSwapPreviewContainer: View { var body: some View { VStack(spacing: 20) { Text(showMax ? "With Max button" : "No Max button — amount stays centered") - .font(Font.dash.caption1) + .dashFont(.caption1) .foregroundColor(Color.dash.tertiaryText) EnterAmountView( @@ -358,7 +358,7 @@ private struct DualSwapPreviewContainer: View { .padding(.horizontal, 20) Button("Tap to swap") { isPrimarySelected.toggle() } - .font(Font.dash.footnote) + .dashFont(.footnote) .foregroundColor(Color.dash.blue) } .padding(.vertical, 20) diff --git a/Sources/DashUIKit/Components/EnterAmount/SwapAmountView.swift b/Sources/DashUIKit/Components/EnterAmount/SwapAmountView.swift index 75293b0..4cb8d36 100644 --- a/Sources/DashUIKit/Components/EnterAmount/SwapAmountView.swift +++ b/Sources/DashUIKit/Components/EnterAmount/SwapAmountView.swift @@ -96,7 +96,7 @@ public struct SwapAmountView: View { VStack(spacing: 2) { if let top = topText { Text(top) - .font(Font.dash.caption1) + .dashFont(.caption1) .foregroundColor(Color.dash.tertiaryText) } @@ -108,7 +108,7 @@ public struct SwapAmountView: View { if let bottom = bottomText { Text(bottom) - .font(Font.dash.caption1) + .dashFont(.caption1) .foregroundColor(Color.dash.tertiaryText) } } @@ -123,7 +123,7 @@ public struct SwapAmountView: View { if let secondary = secondaryText { Text(secondary) - .font(Font.dash.subhead) + .dashFont(.subhead) .foregroundColor(Color.dash.tertiaryText) } } @@ -622,7 +622,7 @@ private struct SwapAmountAnimatedPreview: View { var body: some View { VStack(spacing: 16) { Text(isPrimarySelected ? "Dash is primary (large)" : "Fiat is primary (large)") - .font(Font.dash.caption1) + .dashFont(.caption1) .foregroundColor(Color.dash.tertiaryText) SwapAmountView( @@ -640,7 +640,7 @@ private struct SwapAmountAnimatedPreview: View { .onTapGesture { isPrimarySelected.toggle() } Button("Tap to swap") { isPrimarySelected.toggle() } - .font(Font.dash.footnote) + .dashFont(.footnote) .foregroundColor(Color.dash.blue) } } diff --git a/Sources/DashUIKit/Components/NumericKeyboardView.swift b/Sources/DashUIKit/Components/NumericKeyboardView.swift index 18f2627..b49dfe4 100644 --- a/Sources/DashUIKit/Components/NumericKeyboardView.swift +++ b/Sources/DashUIKit/Components/NumericKeyboardView.swift @@ -198,7 +198,7 @@ public struct NumericKeyboardView: View { private func helperTextRow(_ text: String?) -> some View { if let text, !text.isEmpty { Text(text) - .font(Font.dash.footnote) + .dashFont(.footnote) .foregroundColor(Color.dash.secondaryText) .multilineTextAlignment(.center) .frame(maxWidth: .infinity) diff --git a/Sources/DashUIKit/Components/SearchBar.swift b/Sources/DashUIKit/Components/SearchBar.swift index 603c8e6..3f5471a 100644 --- a/Sources/DashUIKit/Components/SearchBar.swift +++ b/Sources/DashUIKit/Components/SearchBar.swift @@ -142,7 +142,7 @@ private struct SearchBarFocused: View { TextField( text: $text, prompt: Text(placeholder) - .font(Font.dash.subhead) + .dashFont(.subhead) .foregroundStyle(Color.dash.black1000Alpha30) ) { EmptyView() diff --git a/Sources/DashUIKit/Foundation/LineHeight+DashUI.swift b/Sources/DashUIKit/Foundation/LineHeight+DashUI.swift index 94b7417..f53b457 100644 --- a/Sources/DashUIKit/Foundation/LineHeight+DashUI.swift +++ b/Sources/DashUIKit/Foundation/LineHeight+DashUI.swift @@ -67,7 +67,7 @@ public extension View { return VStack(alignment: .leading, spacing: 24) { VStack(alignment: .leading, spacing: 4) { Text("Default (.system natural ≈ 15.5pt)") - .font(Font.dash.caption2) + .dashFont(.caption2) .foregroundColor(Color.dash.secondaryText) Text(sample) .font(Font.dash.footnote) @@ -77,7 +77,7 @@ public extension View { VStack(alignment: .leading, spacing: 4) { Text("dashLineHeight(size: 13, lineHeight: 18)") - .font(Font.dash.caption2) + .dashFont(.caption2) .foregroundColor(Color.dash.secondaryText) Text(sample) .font(Font.dash.footnote) From 640298ffc8cc2118141498f97b99ad66a21a405d Mon Sep 17 00:00:00 2001 From: Roman <51091564+jeanpierreroma@users.noreply.github.com> Date: Wed, 29 Jul 2026 15:51:46 +0300 Subject: [PATCH 5/6] fix(Switch): use black1000Alpha20 for the disabled on state The design system has no track-fill-on-disabled variable, so the disabled on state was a stand-in. Aligns it with the disabled off state, which means a disabled switch reads the same either way and only the thumb position tells them apart. --- Sources/DashUIKit/Components/Switch/SwitchView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/DashUIKit/Components/Switch/SwitchView.swift b/Sources/DashUIKit/Components/Switch/SwitchView.swift index 54ac3fa..aeb15b8 100644 --- a/Sources/DashUIKit/Components/Switch/SwitchView.swift +++ b/Sources/DashUIKit/Components/Switch/SwitchView.swift @@ -12,7 +12,7 @@ public struct SwitchView: View { private struct Constants { static let switchTrackFillOn: Color = Color.dash.blue - static let switchTrackFillOnDisabled: Color = Color.dash.blueAlpha50 + static let switchTrackFillOnDisabled: Color = Color.dash.black1000Alpha20 static let switchTrackFillOff: Color = Color.dash.gray300Alpha50 static let switchTrackFillOffDisabled: Color = Color.dash.black1000Alpha20 static let switchTrackWidth: CGFloat = 64 From 3f868e220e09fb3d5ab9429aa3b9dcf821648a56 Mon Sep 17 00:00:00 2001 From: Roman <51091564+jeanpierreroma@users.noreply.github.com> Date: Wed, 29 Jul 2026 16:43:10 +0300 Subject: [PATCH 6/6] fix(Switch): expose toggle semantics to VoiceOver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SwitchView announced itself as a button with no value, and the tap gesture was not exposed as an accessibility action, so VoiceOver users could not tell the state or change it. Adds a localized On/Off value and an explicit accessibility action. The trait becomes .isToggle where available; that API is iOS 17 and this component must stay usable on iOS 14, so the older path keeps .isButton. No accessibility label is set. SwitchView takes no title, so it has no context to describe itself with — a generic label would also shadow the one the host sets, the same way SwiftUI's own Toggle requires the caller to supply it. --- .../DashUIKit/Components/Switch/SwitchView.swift | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Sources/DashUIKit/Components/Switch/SwitchView.swift b/Sources/DashUIKit/Components/Switch/SwitchView.swift index aeb15b8..579d079 100644 --- a/Sources/DashUIKit/Components/Switch/SwitchView.swift +++ b/Sources/DashUIKit/Components/Switch/SwitchView.swift @@ -19,6 +19,8 @@ public struct SwitchView: View { static let switchTrackPadding: CGFloat = 2 static let switchTrackRadius: CGFloat = 1000 static let animationDuration: Double = 0.2 + static let onValue = NSLocalizedString("On", bundle: .module, comment: "DashUIKit") + static let offValue = NSLocalizedString("Off", bundle: .module, comment: "DashUIKit") } @Environment(\.isEnabled) private var isEnabled @@ -41,7 +43,19 @@ public struct SwitchView: View { .animation(.easeInOut(duration: Constants.animationDuration), value: isOn) .contentShape(Rectangle()) .onTapGesture { isOn.toggle() } - .accessibilityAddTraits(.isButton) + .accessibilityAddTraits(toggleTrait) + .accessibilityValue(Text(isOn ? Constants.onValue : Constants.offValue)) + .accessibilityAction { isOn.toggle() } + } + + /// `.isToggle` is iOS 17, and this component must stay usable on iOS 14, so the + /// older path keeps the button trait. + private var toggleTrait: AccessibilityTraits { + if #available(iOS 17, macOS 14, *) { + return .isToggle + } else { + return .isButton + } } private var trackFill: Color {