diff --git a/PlatformUI/PlatformUI/DesignSystem/Theme/ThemeViewModifiers.swift b/PlatformUI/PlatformUI/DesignSystem/Theme/ThemeViewModifiers.swift index f2d52da3b..b491e7add 100644 --- a/PlatformUI/PlatformUI/DesignSystem/Theme/ThemeViewModifiers.swift +++ b/PlatformUI/PlatformUI/DesignSystem/Theme/ThemeViewModifiers.swift @@ -326,29 +326,35 @@ public enum ClipStyle { } private struct BorderAndClipModifier: ViewModifier { + @EnvironmentObject var themeSettings: ThemeSettings + let style: ClipStyle let borderColor: ThemeColor.SemanticColor let lineWidth: CGFloat func body(content: Content) -> some View { + let color = themeSettings.themeConfig.themeColor.color(of: borderColor) switch style { case .circle: content .clipShape(Circle()) .overlay(Circle() - .strokeBorder(borderColor.color, lineWidth: lineWidth)) + .strokeBorder(color, lineWidth: lineWidth)) + .environmentObject(themeSettings) case .cornerRadius(let cornerRadius): content .clipShape(.rect(cornerRadius: cornerRadius)) .overlay(RoundedRectangle(cornerRadius: cornerRadius) - .strokeBorder(borderColor.color, lineWidth: lineWidth)) + .strokeBorder(color, lineWidth: lineWidth)) + .environmentObject(themeSettings) case .capsule: content .clipShape(Capsule()) .overlay(Capsule() - .strokeBorder(borderColor.color, lineWidth: lineWidth)) + .strokeBorder(color, lineWidth: lineWidth)) + .environmentObject(themeSettings) } } } diff --git a/dydx/dydxPresenters/dydxPresenters/_v4/Markets/dydxMarketsViewBuilder.swift b/dydx/dydxPresenters/dydxPresenters/_v4/Markets/dydxMarketsViewBuilder.swift index 15ba4bfa5..94212f1db 100644 --- a/dydx/dydxPresenters/dydxPresenters/_v4/Markets/dydxMarketsViewBuilder.swift +++ b/dydx/dydxPresenters/dydxPresenters/_v4/Markets/dydxMarketsViewBuilder.swift @@ -130,7 +130,7 @@ private class dydxMarketsViewPresenter: HostedViewPresenter, dydxProfileSecondaryButtonsViewPresenterProtocol { + init(viewModel: dydxProfileSecondaryButtonsViewModel) { super.init() @@ -31,16 +33,23 @@ class dydxProfileSecondaryButtonsViewPresenter: HostedViewPresenter 0 } - } .store(in: &self.subscriptions) } diff --git a/dydx/dydxPresenters/dydxPresenters/_v4/Trade/Margin/dydxAdjustMarginInputViewBuilder.swift b/dydx/dydxPresenters/dydxPresenters/_v4/Trade/Margin/dydxAdjustMarginInputViewBuilder.swift index dbc2a3400..509e8c242 100644 --- a/dydx/dydxPresenters/dydxPresenters/_v4/Trade/Margin/dydxAdjustMarginInputViewBuilder.swift +++ b/dydx/dydxPresenters/dydxPresenters/_v4/Trade/Margin/dydxAdjustMarginInputViewBuilder.swift @@ -110,6 +110,8 @@ private class dydxAdjustMarginInputViewPresenter: HostedViewPresenter Void) = { + private lazy var doneAction: (() -> Void) = { [weak self] in let notificationPermission = NotificationService.shared?.authorization if notificationPermission?.authorization == .notDetermined { - Router.shared?.navigate(to: RoutingRequest(path: "/action/dismiss"), animated: true, completion: { _, _ in + self?.dismissView { Router.shared?.navigate(to: RoutingRequest(path: "/authorization/notification", params: nil), animated: true, completion: nil) - }) + } } else { - Router.shared?.navigate(to: RoutingRequest(path: "/action/dismiss"), animated: true, completion: nil) + self?.dismissView(completion: nil) + } + } + + private func dismissView(completion: (() -> Void)?) { + switch tradeType { + case .trade: + Router.shared?.navigate(to: RoutingRequest(path: "/action/dismiss"), animated: true) { + _, _ in + completion?() + } + case .closePosition: + Router.shared?.navigate(to: RoutingRequest(path: "/action/dismiss"), animated: true) { + _, _ in + Router.shared?.navigate(to: RoutingRequest(path: "/action/dismiss"), animated: true) { _, _ in + completion?() + } + } } } diff --git a/dydx/dydxPresenters/dydxPresenters/_v4/Vault/DepositsAndWithdrawals/dydxVaultDepositWithdrawViewBuilder.swift b/dydx/dydxPresenters/dydxPresenters/_v4/Vault/DepositsAndWithdrawals/dydxVaultDepositWithdrawViewBuilder.swift index ea95d5005..5e0a5cc81 100644 --- a/dydx/dydxPresenters/dydxPresenters/_v4/Vault/DepositsAndWithdrawals/dydxVaultDepositWithdrawViewBuilder.swift +++ b/dydx/dydxPresenters/dydxPresenters/_v4/Vault/DepositsAndWithdrawals/dydxVaultDepositWithdrawViewBuilder.swift @@ -73,24 +73,38 @@ private class dydxVaultDepositWithdrawViewPresenter: HostedViewPresenter rhs.marginUsdc?.doubleValue ?? 0 } - .map { (position) -> dydxVaultPositionViewModel? in + .compactMap { (position) -> dydxVaultPositionViewModel? in guard - let marketId = position.marketId, - // special case for fake USDC market to show unused margin - let assetId = marketId == "UNALLOCATEDUSDC-USD" ? "USDC" : marketMap[marketId]?.assetId + let marketId = position.marketId else { return nil } + // special case for fake USDC market to show unused margin + let assetId = marketId == "UNALLOCATEDUSDC-USD" ? "USDC" : marketMap[marketId]?.assetId let leverage = position.currentLeverageMultiple?.doubleValue - let asset = assetMap[assetId] + let asset: Asset? + if let assetId = assetId { + asset = assetMap[assetId] + } else { + asset = nil + } let equity = position.marginUsdc?.doubleValue ?? 0 let notionalValue = position.currentPosition?.usdc?.doubleValue ?? 0 let positionSize = position.currentPosition?.asset?.doubleValue ?? 0 @@ -129,7 +134,7 @@ private class dydxVaultViewBuilderPresenter: HostedViewPresenter + + + + + + + + + + + diff --git a/dydx/dydxViews/dydxViews/Media.xcassets/icon_earn.imageset/icon_earn.pdf b/dydx/dydxViews/dydxViews/Media.xcassets/icon_earn.imageset/icon_earn.pdf deleted file mode 100644 index 8116bc9f5..000000000 Binary files a/dydx/dydxViews/dydxViews/Media.xcassets/icon_earn.imageset/icon_earn.pdf and /dev/null differ diff --git a/dydx/dydxViews/dydxViews/Media.xcassets/icon_market.imageset/Contents.json b/dydx/dydxViews/dydxViews/Media.xcassets/icon_market.imageset/Contents.json index eb657bfe5..46d18a644 100644 --- a/dydx/dydxViews/dydxViews/Media.xcassets/icon_market.imageset/Contents.json +++ b/dydx/dydxViews/dydxViews/Media.xcassets/icon_market.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "icon_market.pdf", + "filename" : "Vector.svg", "idiom" : "universal" } ], diff --git a/dydx/dydxViews/dydxViews/Media.xcassets/icon_market.imageset/Vector.svg b/dydx/dydxViews/dydxViews/Media.xcassets/icon_market.imageset/Vector.svg new file mode 100644 index 000000000..70c36e02c --- /dev/null +++ b/dydx/dydxViews/dydxViews/Media.xcassets/icon_market.imageset/Vector.svg @@ -0,0 +1,3 @@ + + + diff --git a/dydx/dydxViews/dydxViews/Media.xcassets/icon_market.imageset/icon_market.pdf b/dydx/dydxViews/dydxViews/Media.xcassets/icon_market.imageset/icon_market.pdf deleted file mode 100644 index 8b2a4f00c..000000000 Binary files a/dydx/dydxViews/dydxViews/Media.xcassets/icon_market.imageset/icon_market.pdf and /dev/null differ diff --git a/dydx/dydxViews/dydxViews/Media.xcassets/icon_portfolio.imageset/Contents.json b/dydx/dydxViews/dydxViews/Media.xcassets/icon_portfolio.imageset/Contents.json index 624a796dc..97dc67bf3 100644 --- a/dydx/dydxViews/dydxViews/Media.xcassets/icon_portfolio.imageset/Contents.json +++ b/dydx/dydxViews/dydxViews/Media.xcassets/icon_portfolio.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "icon_portfolio.pdf", + "filename" : "Subtract.svg", "idiom" : "universal" } ], diff --git a/dydx/dydxViews/dydxViews/Media.xcassets/icon_portfolio.imageset/Subtract.svg b/dydx/dydxViews/dydxViews/Media.xcassets/icon_portfolio.imageset/Subtract.svg new file mode 100644 index 000000000..111830d54 --- /dev/null +++ b/dydx/dydxViews/dydxViews/Media.xcassets/icon_portfolio.imageset/Subtract.svg @@ -0,0 +1,3 @@ + + + diff --git a/dydx/dydxViews/dydxViews/Media.xcassets/icon_portfolio.imageset/icon_portfolio.pdf b/dydx/dydxViews/dydxViews/Media.xcassets/icon_portfolio.imageset/icon_portfolio.pdf deleted file mode 100644 index 8c0750783..000000000 Binary files a/dydx/dydxViews/dydxViews/Media.xcassets/icon_portfolio.imageset/icon_portfolio.pdf and /dev/null differ diff --git a/dydx/dydxViews/dydxViews/Media.xcassets/icon_profile.imageset/Contents.json b/dydx/dydxViews/dydxViews/Media.xcassets/icon_profile.imageset/Contents.json index 8cb596c0a..46d18a644 100644 --- a/dydx/dydxViews/dydxViews/Media.xcassets/icon_profile.imageset/Contents.json +++ b/dydx/dydxViews/dydxViews/Media.xcassets/icon_profile.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "icon_profile.pdf", + "filename" : "Vector.svg", "idiom" : "universal" } ], diff --git a/dydx/dydxViews/dydxViews/Media.xcassets/icon_profile.imageset/Vector.svg b/dydx/dydxViews/dydxViews/Media.xcassets/icon_profile.imageset/Vector.svg new file mode 100644 index 000000000..ad990e955 --- /dev/null +++ b/dydx/dydxViews/dydxViews/Media.xcassets/icon_profile.imageset/Vector.svg @@ -0,0 +1,3 @@ + + + diff --git a/dydx/dydxViews/dydxViews/Media.xcassets/icon_profile.imageset/icon_profile.pdf b/dydx/dydxViews/dydxViews/Media.xcassets/icon_profile.imageset/icon_profile.pdf deleted file mode 100644 index 209b37afb..000000000 Binary files a/dydx/dydxViews/dydxViews/Media.xcassets/icon_profile.imageset/icon_profile.pdf and /dev/null differ diff --git a/dydx/dydxViews/dydxViews/Shared/dydxComponents/dydxTitledNumberField.swift b/dydx/dydxViews/dydxViews/Shared/dydxComponents/dydxTitledNumberField.swift index 489f11586..16667dc22 100644 --- a/dydx/dydxViews/dydxViews/Shared/dydxComponents/dydxTitledNumberField.swift +++ b/dydx/dydxViews/dydxViews/Shared/dydxComponents/dydxTitledNumberField.swift @@ -85,6 +85,7 @@ struct dydxTitledNumberField: View { } textFieldView } + if isMaxButtonVisible { Spacer() maxButton @@ -123,7 +124,7 @@ private struct NumberTextField: View { .keyboardType(keyboardType) .focused($isFocused) .onChange(of: isFocused) { _ in - if !isFocused, let value = Double(filteredTextBinding.wrappedValue) { + if !isFocused, let value = Parser.standard.asInputDecimal(filteredTextBinding.wrappedValue)?.doubleValue { actualValue = formatValue(value) } } @@ -149,7 +150,7 @@ private struct NumberTextField: View { } }, set: { newValue in - if let doubleValue = Double(newValue) { + if let doubleValue = Parser.standard.asInputDecimal(newValue)?.doubleValue { actualValue = formatValue(clamp(doubleValue)) } else { actualValue = nil diff --git a/dydx/dydxViews/dydxViews/_v4/Markets/Components/dydxMarketListView.swift b/dydx/dydxViews/dydxViews/_v4/Markets/Components/dydxMarketListView.swift index 1e9937b1c..e6daa4771 100644 --- a/dydx/dydxViews/dydxViews/_v4/Markets/Components/dydxMarketListView.swift +++ b/dydx/dydxViews/dydxViews/_v4/Markets/Components/dydxMarketListView.swift @@ -26,14 +26,14 @@ public class dydxMarketListViewModel: PlatformViewModeling { iconUrl: "https://assets.coingecko.com/coins/images/1/large/bitcoin.png", volume24H: 1_000_000_000, sparkline: [1, 2, 3, 4, 5], - price: 50_000, + price: "50_000", change: 0.05), dydxMarketViewModel(marketId: "ETH-USD", assetId: "ETH", iconUrl: "https://assets.coingecko.com/coins/images/279/large/ethereum.png", volume24H: 500_000_000, sparkline: [5, 4, 3, 2, 1], - price: 3_000, + price: "3_000", change: -0.05) ] return vm @@ -74,7 +74,7 @@ public class dydxMarketViewModel: PlatformViewModeling { public let iconUrl: String? public let volume24H: Double? public let sparkline: [Double]? - public let price: Double? + public let price: String? public let change: Double? @Published public var isFavorite: Bool? @@ -87,7 +87,7 @@ public class dydxMarketViewModel: PlatformViewModeling { iconUrl: String?, volume24H: Double?, sparkline: [Double]?, - price: Double?, + price: String?, change: Double?, isFavorite: Bool? = nil ) { @@ -147,7 +147,7 @@ struct dydxMarketView: View { var priceChangeVStack: some View { VStack(alignment: .trailing, spacing: 2) { - Text(dydxFormatter.shared.dollar(number: viewModel.price) ?? "--") + Text(viewModel.price ?? "--") .themeFont(fontType: .plus, fontSize: .medium) .themeColor(foreground: .textPrimary) SignedAmountViewModel(amount: viewModel.change, displayType: .percent, coloringOption: .allText) diff --git a/dydx/dydxViews/dydxViews/_v4/Profile/Components/dydxProfileSecondaryButtonsView.swift b/dydx/dydxViews/dydxViews/_v4/Profile/Components/dydxProfileSecondaryButtonsView.swift index d8c846748..0e85414b5 100644 --- a/dydx/dydxViews/dydxViews/_v4/Profile/Components/dydxProfileSecondaryButtonsView.swift +++ b/dydx/dydxViews/dydxViews/_v4/Profile/Components/dydxProfileSecondaryButtonsView.swift @@ -13,6 +13,7 @@ public class dydxProfileSecondaryButtonsViewModel: PlatformViewModel { @Published public var settingsAction: (() -> Void)? @Published public var helpAction: (() -> Void)? @Published public var alertsAction: (() -> Void)? + @Published public var hasNewAlerts: Bool = false public init() { } @@ -39,9 +40,34 @@ public class dydxProfileSecondaryButtonsViewModel: PlatformViewModel { @ViewBuilder private var alertsRow: some View { if let alertsAction { - self.createButton(imageName: "icon_alerts", - title: DataLocalizer.localize(path: "APP.GENERAL.ALERTS"), - action: alertsAction) + HStack(spacing: 8) { + PlatformIconViewModel(type: .asset(name: "icon_alerts", bundle: Bundle.dydxView), + clip: .noClip, + size: CGSize(width: 24, height: 24), + templateColor: .textTertiary) + .createView() + + Text(DataLocalizer.localize(path: "APP.GENERAL.ALERTS")) + .themeFont(fontSize: .medium) + .themeColor(foreground: .textPrimary) + .lineLimit(1) + + Spacer() + + if hasNewAlerts { + Circle() + .fill(ThemeColor.SemanticColor.colorPurple.color) + .frame(width: 10, height: 10) + } + } + .padding(.horizontal, 16) + .padding(.vertical, 22) + .themeColor(background: .layer3) + .cornerRadius(12, corners: .allCorners) + .frame(maxWidth: .infinity) + .onTapGesture { + alertsAction() + } } } diff --git a/dydx/dydxViews/dydxViews/_v4/Vault/DepositAndWithdrawal/VaultTransferType.swift b/dydx/dydxViews/dydxViews/_v4/Vault/DepositAndWithdrawal/VaultTransferType.swift index 5870e745f..9d6c508f3 100644 --- a/dydx/dydxViews/dydxViews/_v4/Vault/DepositAndWithdrawal/VaultTransferType.swift +++ b/dydx/dydxViews/dydxViews/_v4/Vault/DepositAndWithdrawal/VaultTransferType.swift @@ -72,7 +72,7 @@ public enum VaultTransferType: CaseIterable, RadioButtonContentDisplayable { var transferDestinationSubtitleText: String { switch self { - case .deposit: return DataLocalizer.localize(path: "APP.VAULTS.PROTOCOL_VAULT") + case .deposit: return DataLocalizer.localize(path: "APP.VAULTS.MEGAVAULT") case .withdraw: return DataLocalizer.localize(path: "APP.VAULTS.CROSS_ACCOUNT") } } diff --git a/dydx/dydxViews/dydxViews/_v4/Vault/Landing/dydxVaultChartViewModel.swift b/dydx/dydxViews/dydxViews/_v4/Vault/Landing/dydxVaultChartViewModel.swift index 00ce0ee65..4f7994991 100644 --- a/dydx/dydxViews/dydxViews/_v4/Vault/Landing/dydxVaultChartViewModel.swift +++ b/dydx/dydxViews/dydxViews/_v4/Vault/Landing/dydxVaultChartViewModel.swift @@ -12,6 +12,7 @@ import Utilities import Charts import Combine import dydxChart +import dydxFormatter public class dydxVaultChartViewModel: PlatformViewModel { public struct Entry { @@ -53,19 +54,19 @@ public class dydxVaultChartViewModel: PlatformViewModel { } public enum ValueTimeOption: CaseIterable, RadioButtonContentDisplayable { - case oneDay case sevenDays case thirtyDays + case ninetyDays var displayText: String { let path: String switch self { - case .oneDay: - path = "APP.GENERAL.TIME_STRINGS.1D" case .sevenDays: path = "APP.GENERAL.TIME_STRINGS.7D" case .thirtyDays: path = "APP.GENERAL.TIME_STRINGS._30D" + case .ninetyDays: + path = "APP.GENERAL.TIME_STRINGS.90D" } return DataLocalizer.shared?.localize(path: path, params: nil) ?? "" } @@ -111,6 +112,7 @@ private struct dydxVaultChartView: View { .frame(minWidth: geometry.size.width, alignment: .center) } } + .frame(height: 38) } private var chart: some View { @@ -119,7 +121,7 @@ private struct dydxVaultChartView: View { y: .value("", entry.value)) .lineStyle(StrokeStyle(lineWidth: 2)) .foregroundStyle(viewModel.lineColor.gradient) - .interpolationMethod(.linear) + .interpolationMethod(.monotone) .symbolSize(0) // adds gradient shading AreaMark( @@ -130,16 +132,24 @@ private struct dydxVaultChartView: View { .foregroundStyle(chartGradient) } .chartXAxis(.hidden) - .chartYAxis(.hidden) + .chartYAxis { + AxisMarks(values: .automatic) { + let value = dydxFormatter.shared.condensedDollar(number: $0.as(Double.self)) + AxisValueLabel { + if let value { + Text(value) + } + } + .font(Font.system(size: 8)) + .foregroundStyle(ThemeColor.SemanticColor.textTertiary.color) + } + } .chartXScale(domain: .automatic(includesZero: false)) .chartYScale(domain: .automatic(includesZero: false)) - - // the lines can extend outside of chart - .padding(.all, 1) } var body: some View { - VStack(spacing: 12) { + VStack(spacing: 8) { radioButtonsRow chart } diff --git a/dydx/dydxViews/dydxViews/_v4/Vault/Landing/dydxVaultPositionView.swift b/dydx/dydxViews/dydxViews/_v4/Vault/Landing/dydxVaultPositionView.swift index 3fc18989e..80af20a38 100644 --- a/dydx/dydxViews/dydxViews/_v4/Vault/Landing/dydxVaultPositionView.swift +++ b/dydx/dydxViews/dydxViews/_v4/Vault/Landing/dydxVaultPositionView.swift @@ -33,11 +33,16 @@ public class dydxVaultPositionViewModel: PlatformViewModel { fileprivate var sideLeverageAttributedText: AttributedString { let attributedSideText = AttributedString(text: side.text, urlString: nil) .themeColor(foreground: side.color) - let leverageText = dydxFormatter.shared.leverage(number: leverage) ?? "--" - let attributedLeverageText = AttributedString(text: "@ " + leverageText, urlString: nil) - .themeColor(foreground: .textTertiary) - return (attributedSideText + attributedLeverageText) + if let leverage = leverage { + let leverageText = dydxFormatter.shared.leverage(number: leverage) ?? "--" + let attributedLeverageText = AttributedString(text: " @ " + leverageText, urlString: nil) + .themeColor(foreground: .textTertiary) + return (attributedSideText + attributedLeverageText) .themeFont(fontType: .base, fontSize: .smaller) + } else { + return attributedSideText + .themeFont(fontType: .base, fontSize: .smaller) + } } fileprivate var notionalValueText: String { @@ -175,10 +180,10 @@ struct VaultPositionView: View { var body: some View { HStack(spacing: Self.interSectionPadding) { - marketSection - .frame(width: Self.marketSectionWidth) - sizeSection - pnlSection - } + marketSection + .frame(width: Self.marketSectionWidth) + sizeSection + pnlSection + } } } diff --git a/dydx/dydxViews/dydxViews/_v4/Vault/Landing/dydxVaultViewModel.swift b/dydx/dydxViews/dydxViews/_v4/Vault/Landing/dydxVaultViewModel.swift index 3bc82f261..18ebf80cf 100644 --- a/dydx/dydxViews/dydxViews/_v4/Vault/Landing/dydxVaultViewModel.swift +++ b/dydx/dydxViews/dydxViews/_v4/Vault/Landing/dydxVaultViewModel.swift @@ -155,7 +155,7 @@ private struct dydxVaultView: View { Spacer(minLength: 4) Text(dydxFormatter.shared.dollar(number: viewModel.vaultBalance) ?? "--") .themeColor(foreground: .textPrimary) - .themeFont(fontType: .base, fontSize: .medium) + .themeFont(fontType: .base, fontSize: .large) } .leftAligned() .padding(.horizontal, 16) @@ -171,7 +171,7 @@ private struct dydxVaultView: View { Spacer(minLength: 4) Text(dydxFormatter.shared.dollar(number: viewModel.allTimeReturnUsdc) ?? "--") .themeColor(foreground: viewModel.allTimeReturnUsdc == nil ? .textPrimary : ThemeSettings.directionalColor(forValue: viewModel.allTimeReturnUsdc)) - .themeFont(fontType: .base, fontSize: .medium) + .themeFont(fontType: .base, fontSize: .large) } .leftAligned() .padding(.horizontal, 16) @@ -196,7 +196,7 @@ private struct dydxVaultView: View { .themeFont(fontType: .base, fontSize: .small) Text(dydxFormatter.shared.percent(number: viewModel.thirtyDayReturnPercent, digits: 0) ?? "") .themeColor(foreground: ThemeSettings.directionalColor(forValue: viewModel.thirtyDayReturnPercent)) - .themeFont(fontType: .base, fontSize: .medium) + .themeFont(fontType: .base, fontSize: .large) } } @@ -207,7 +207,7 @@ private struct dydxVaultView: View { .themeFont(fontType: .base, fontSize: .small) Text(dydxFormatter.shared.dollar(number: viewModel.totalValueLocked, digits: 0) ?? "") .themeColor(foreground: .textPrimary) - .themeFont(fontType: .base, fontSize: .medium) + .themeFont(fontType: .base, fontSize: .large) } } @@ -221,7 +221,7 @@ private struct dydxVaultView: View { // MARK: - Section 4 - positions private var openPositionsHeader: some View { HStack(spacing: 8) { - Text(DataLocalizer.shared?.localize(path: "APP.TRADE.OPEN_POSITIONS", params: nil) ?? "") + Text(DataLocalizer.shared?.localize(path: "APP.VAULTS.HOLDINGS", params: nil) ?? "") .themeColor(foreground: .textSecondary) .themeFont(fontType: .base, fontSize: .larger) Text("\(viewModel.positions?.count ?? 0)")