From be0516c6fa52bc0982ca22e73ec24a3d145598b0 Mon Sep 17 00:00:00 2001 From: Peter-John Welcome Date: Thu, 27 Aug 2026 13:46:59 +0200 Subject: [PATCH] Add semantic colour tokens and dark mode support Introduce a Semantic colour set (surface/border/content/accent/state) backed by light+dark values in Colors.xcassets, exposed via Color extensions, and migrate every PaystackUI view off literal colours and legacy hue tokens. Also adds DarkModePreviews for visual checks and two SwiftLint custom rules that flag literal colours and direct hue-token use in view code. --- .swiftlint.yml | 20 ++ .../BankTransfer/Views/BankPickerSheet.swift | 14 +- .../BankTransferAccountDetailsView.swift | 14 +- .../Views/BankTransferConfirmingView.swift | 10 +- .../BankTransferDelayedConfirmationView.swift | 10 +- .../BankTransferRefundInitiatedView.swift | 6 +- .../Views/BankTransferTakingLongerView.swift | 8 +- .../ChargeCard/Address/CardAddressView.swift | 2 +- .../Birthday/CardBirthdayView.swift | 2 +- .../Charge/ChargeCard/OTP/CardOTPVIew.swift | 2 +- .../ChargeCard/Phone/CardPhoneView.swift | 2 +- .../Charge/ChargeCard/Pin/CardPinView.swift | 6 +- .../Redirect/CardRedirectView.swift | 6 +- .../TestMode/TestModeCardSelectionView.swift | 4 +- Sources/PaystackUI/Charge/ChargeView.swift | 2 + .../Views/ChannelSelectionView.swift | 6 +- .../Views/MobileMoneyChargeView.swift | 2 +- .../Views/MobileMoneyProcessingView.swift | 10 +- .../Charge/Success/ChargeSuccessView.swift | 4 +- .../Charge/Zap/Components/QRCodeImage.swift | 10 +- .../Charge/Zap/Views/ZapPaymentView.swift | 12 +- .../Zap/Views/ZapSessionExpiredView.swift | 4 +- .../Components/AccountDetailRow.swift | 8 +- .../ChangePaymentMethodFooter.swift | 4 +- Sources/PaystackUI/Components/ErrorView.swift | 2 +- .../Components/Form/FormInput.swift | 2 +- .../Components/Form/FormTextFieldStyle.swift | 2 +- .../Components/Form/PickerFormInputView.swift | 6 +- .../Form/TextFieldFocusedBorder.swift | 4 +- .../Form/TextFieldFormInputView.swift | 2 +- .../Components/Form/TitleFormInputView.swift | 2 +- .../Validation/ValidationFormInputView.swift | 2 +- .../Components/LoadingIndicator.swift | 2 +- .../PaystackUI/Components/LoadingView.swift | 4 +- .../Components/ModalCancelButton.swift | 2 +- .../SwiftUI/Core/PinTextView.swift | 6 +- .../Core/PinTextViewRepresentable.swift | 4 +- .../PinTextView/UIKit/Core/PinTextField.swift | 10 +- .../Styles/PrimaryButtonStyle.swift | 18 +- .../Styles/SecondaryButtonStyle.swift | 8 +- .../Components/TestModeInidcator.swift | 6 +- .../PaystackUI/Components/TimelineNode.swift | 10 +- .../Views/SingleSelectionFormInputView.swift | 2 +- .../AccentPrimary.colorset/Contents.json | 38 +++ .../Contents.json | 38 +++ .../Contents.json | 38 +++ .../AccentSecondary.colorset/Contents.json | 38 +++ .../BorderError.colorset/Contents.json | 38 +++ .../BorderPrimary.colorset/Contents.json | 38 +++ .../BorderStrong.colorset/Contents.json | 38 +++ .../BorderSubtle.colorset/Contents.json | 38 +++ .../ContentError.colorset/Contents.json | 38 +++ .../ContentOnAccent.colorset/Contents.json | 38 +++ .../Contents.json | 38 +++ .../ContentPlaceholder.colorset/Contents.json | 38 +++ .../ContentPrimary.colorset/Contents.json | 38 +++ .../ContentSecondary.colorset/Contents.json | 38 +++ .../ContentTertiary.colorset/Contents.json | 38 +++ .../ContentWarning.colorset/Contents.json | 38 +++ .../Colors.xcassets/Semantic/Contents.json | 6 + .../Semantic/IconMuted.colorset/Contents.json | 38 +++ .../InfoSurface.colorset/Contents.json | 38 +++ .../OverlayScrim.colorset/Contents.json | 38 +++ .../PinSlotBorder.colorset/Contents.json | 38 +++ .../PinSlotSurface.colorset/Contents.json | 38 +++ .../Semantic/QRPlate.colorset/Contents.json | 38 +++ .../ShimmerBase.colorset/Contents.json | 38 +++ .../ShimmerHighlight.colorset/Contents.json | 38 +++ .../SpinnerTint.colorset/Contents.json | 38 +++ .../Contents.json | 38 +++ .../SurfacePrimary.colorset/Contents.json | 38 +++ .../SurfaceSecondary.colorset/Contents.json | 38 +++ .../SurfaceTertiary.colorset/Contents.json | 38 +++ .../TestModeBorder.colorset/Contents.json | 38 +++ .../TestModeContent.colorset/Contents.json | 38 +++ .../TestModeSurface.colorset/Contents.json | 38 +++ .../ToastSurface.colorset/Contents.json | 38 +++ .../TrackInactive.colorset/Contents.json | 38 +++ .../WarningSurface.colorset/Contents.json | 38 +++ Sources/PaystackUI/Design/Colors.swift | 217 +++++++++++++++++ .../PaystackUI/Design/DarkModePreviews.swift | 222 ++++++++++++++++++ Sources/PaystackUI/Images/Images.swift | 8 +- 82 files changed, 1922 insertions(+), 123 deletions(-) create mode 100644 Sources/PaystackUI/Design/ColorAssets/Colors.xcassets/Semantic/AccentPrimary.colorset/Contents.json create mode 100644 Sources/PaystackUI/Design/ColorAssets/Colors.xcassets/Semantic/AccentPrimaryDisabled.colorset/Contents.json create mode 100644 Sources/PaystackUI/Design/ColorAssets/Colors.xcassets/Semantic/AccentPrimaryPressed.colorset/Contents.json create mode 100644 Sources/PaystackUI/Design/ColorAssets/Colors.xcassets/Semantic/AccentSecondary.colorset/Contents.json create mode 100644 Sources/PaystackUI/Design/ColorAssets/Colors.xcassets/Semantic/BorderError.colorset/Contents.json create mode 100644 Sources/PaystackUI/Design/ColorAssets/Colors.xcassets/Semantic/BorderPrimary.colorset/Contents.json create mode 100644 Sources/PaystackUI/Design/ColorAssets/Colors.xcassets/Semantic/BorderStrong.colorset/Contents.json create mode 100644 Sources/PaystackUI/Design/ColorAssets/Colors.xcassets/Semantic/BorderSubtle.colorset/Contents.json create mode 100644 Sources/PaystackUI/Design/ColorAssets/Colors.xcassets/Semantic/ContentError.colorset/Contents.json create mode 100644 Sources/PaystackUI/Design/ColorAssets/Colors.xcassets/Semantic/ContentOnAccent.colorset/Contents.json create mode 100644 Sources/PaystackUI/Design/ColorAssets/Colors.xcassets/Semantic/ContentOnAccentPressed.colorset/Contents.json create mode 100644 Sources/PaystackUI/Design/ColorAssets/Colors.xcassets/Semantic/ContentPlaceholder.colorset/Contents.json create mode 100644 Sources/PaystackUI/Design/ColorAssets/Colors.xcassets/Semantic/ContentPrimary.colorset/Contents.json create mode 100644 Sources/PaystackUI/Design/ColorAssets/Colors.xcassets/Semantic/ContentSecondary.colorset/Contents.json create mode 100644 Sources/PaystackUI/Design/ColorAssets/Colors.xcassets/Semantic/ContentTertiary.colorset/Contents.json create mode 100644 Sources/PaystackUI/Design/ColorAssets/Colors.xcassets/Semantic/ContentWarning.colorset/Contents.json create mode 100644 Sources/PaystackUI/Design/ColorAssets/Colors.xcassets/Semantic/Contents.json create mode 100644 Sources/PaystackUI/Design/ColorAssets/Colors.xcassets/Semantic/IconMuted.colorset/Contents.json create mode 100644 Sources/PaystackUI/Design/ColorAssets/Colors.xcassets/Semantic/InfoSurface.colorset/Contents.json create mode 100644 Sources/PaystackUI/Design/ColorAssets/Colors.xcassets/Semantic/OverlayScrim.colorset/Contents.json create mode 100644 Sources/PaystackUI/Design/ColorAssets/Colors.xcassets/Semantic/PinSlotBorder.colorset/Contents.json create mode 100644 Sources/PaystackUI/Design/ColorAssets/Colors.xcassets/Semantic/PinSlotSurface.colorset/Contents.json create mode 100644 Sources/PaystackUI/Design/ColorAssets/Colors.xcassets/Semantic/QRPlate.colorset/Contents.json create mode 100644 Sources/PaystackUI/Design/ColorAssets/Colors.xcassets/Semantic/ShimmerBase.colorset/Contents.json create mode 100644 Sources/PaystackUI/Design/ColorAssets/Colors.xcassets/Semantic/ShimmerHighlight.colorset/Contents.json create mode 100644 Sources/PaystackUI/Design/ColorAssets/Colors.xcassets/Semantic/SpinnerTint.colorset/Contents.json create mode 100644 Sources/PaystackUI/Design/ColorAssets/Colors.xcassets/Semantic/SurfaceInsetTranslucent.colorset/Contents.json create mode 100644 Sources/PaystackUI/Design/ColorAssets/Colors.xcassets/Semantic/SurfacePrimary.colorset/Contents.json create mode 100644 Sources/PaystackUI/Design/ColorAssets/Colors.xcassets/Semantic/SurfaceSecondary.colorset/Contents.json create mode 100644 Sources/PaystackUI/Design/ColorAssets/Colors.xcassets/Semantic/SurfaceTertiary.colorset/Contents.json create mode 100644 Sources/PaystackUI/Design/ColorAssets/Colors.xcassets/Semantic/TestModeBorder.colorset/Contents.json create mode 100644 Sources/PaystackUI/Design/ColorAssets/Colors.xcassets/Semantic/TestModeContent.colorset/Contents.json create mode 100644 Sources/PaystackUI/Design/ColorAssets/Colors.xcassets/Semantic/TestModeSurface.colorset/Contents.json create mode 100644 Sources/PaystackUI/Design/ColorAssets/Colors.xcassets/Semantic/ToastSurface.colorset/Contents.json create mode 100644 Sources/PaystackUI/Design/ColorAssets/Colors.xcassets/Semantic/TrackInactive.colorset/Contents.json create mode 100644 Sources/PaystackUI/Design/ColorAssets/Colors.xcassets/Semantic/WarningSurface.colorset/Contents.json create mode 100644 Sources/PaystackUI/Design/DarkModePreviews.swift diff --git a/.swiftlint.yml b/.swiftlint.yml index 02769e0..19f6427 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -3,3 +3,23 @@ excluded: # paths to ignore during linting. Takes precedence over `included`. - "*/*/GeneratedMocks.swift" - "*/*/*.generated.swift" - ".build/" + +custom_rules: + paystack_ui_no_literal_colour: + name: "PaystackUI literal colour" + regex: "(Color\\.(white|black|gray|red|blue|green)\\b|Color\\(red:\\s|UIColor\\(red:\\s)" + message: "Use a semantic token from Design/Colors.swift instead of a literal colour. Fixed exceptions (e.g. the QR quiet-zone plate) must carry a `// swiftlint:disable:next paystack_ui_no_literal_colour` comment explaining why." + included: "Sources/PaystackUI/.*\\.swift" + excluded: + - "Sources/PaystackUI/Design/Colors.swift" + - "Sources/PaystackUI/Design/DarkModePreviews.swift" + severity: warning + paystack_ui_no_hue_token: + name: "PaystackUI legacy hue token" + regex: "Color\\.(stackBlue|navy0[2-5]|stackGreen|green02|gray01|error0[12]|warning0[1245])\\b" + message: "Use the semantic token layer (contentPrimary, borderSubtle, accentPrimary, …) instead of the underlying hue token. Hue tokens are kept as internal primitives per Q8 in DARK_MODE_IMPLEMENTATION_PLAN.md and should not be called directly by view code." + included: "Sources/PaystackUI/.*\\.swift" + excluded: + - "Sources/PaystackUI/Design/Colors.swift" + - "Sources/PaystackUI/Design/DarkModePreviews.swift" + severity: warning diff --git a/Sources/PaystackUI/Charge/BankTransfer/Views/BankPickerSheet.swift b/Sources/PaystackUI/Charge/BankTransfer/Views/BankPickerSheet.swift index 926018a..2e88d1f 100644 --- a/Sources/PaystackUI/Charge/BankTransfer/Views/BankPickerSheet.swift +++ b/Sources/PaystackUI/Charge/BankTransfer/Views/BankPickerSheet.swift @@ -14,7 +14,7 @@ struct BankPickerSheet: View { header Divider() - .background(Color.navy05) + .background(Color.borderPrimary) ScrollView { LazyVStack(spacing: 0) { @@ -24,24 +24,26 @@ struct BankPickerSheet: View { isSelected: slug == currentSlug, onTap: { onSelect(slug) }) Divider() - .background(Color.navy05) + .background(Color.borderPrimary) } } } Spacer(minLength: 0) } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.surfacePrimary.ignoresSafeArea()) } private var header: some View { HStack { Text("Change bank") .font(.heading3) - .foregroundColor(.stackBlue) + .foregroundColor(.contentPrimary) Spacer() Button(action: onCancel) { Image(systemName: "xmark") - .foregroundColor(.navy02) + .foregroundColor(.contentSecondary) } } .padding(.doublePadding) @@ -60,11 +62,11 @@ private struct BankPickerRow: View { HStack { Text(displayName) .font(.body16M) - .foregroundColor(.stackBlue) + .foregroundColor(.contentPrimary) Spacer() if isSelected { Image(systemName: "checkmark") - .foregroundColor(.stackGreen) + .foregroundColor(.accentPrimary) } } .padding(.doublePadding) diff --git a/Sources/PaystackUI/Charge/BankTransfer/Views/BankTransferAccountDetailsView.swift b/Sources/PaystackUI/Charge/BankTransfer/Views/BankTransferAccountDetailsView.swift index e00fadd..ab0d408 100644 --- a/Sources/PaystackUI/Charge/BankTransfer/Views/BankTransferAccountDetailsView.swift +++ b/Sources/PaystackUI/Charge/BankTransfer/Views/BankTransferAccountDetailsView.swift @@ -31,7 +31,7 @@ struct BankTransferAccountDetailsView: View { Text("Transfer exactly \(amount.description) to") .font(.heading3) - .foregroundColor(.stackBlue) + .foregroundColor(.contentPrimary) .multilineTextAlignment(.center) accountCard @@ -42,7 +42,7 @@ struct BankTransferAccountDetailsView: View { .buttonStyle(PrimaryButtonStyle(showLoading: false)) Button("Change payment method", action: onChangePaymentMethod) - .foregroundColor(.navy02) + .foregroundColor(.contentSecondary) .font(.body14M) .padding(.top, .singlePadding) } @@ -74,13 +74,13 @@ struct BankTransferAccountDetailsView: View { } .overlay( RoundedRectangle(cornerRadius: .cornerRadius) - .stroke(Color.navy05, lineWidth: 1) + .stroke(Color.borderPrimary, lineWidth: 1) ) } private var divider: some View { Rectangle() - .fill(Color.navy05) + .fill(Color.surfaceSecondary) .frame(height: 1) } @@ -88,15 +88,15 @@ struct BankTransferAccountDetailsView: View { VStack(spacing: .singlePadding) { Text("This account is for one-time use only and expires in") .font(.body14R) - .foregroundColor(.navy03) + .foregroundColor(.contentTertiary) .multilineTextAlignment(.center) Text(formattedRemaining) .font(.body14M) - .foregroundColor(.stackGreen) + .foregroundColor(.accentPrimary) ProgressView(value: progress) .progressViewStyle(.linear) - .accentColor(.stackGreen) + .accentColor(.accentPrimary) } } diff --git a/Sources/PaystackUI/Charge/BankTransfer/Views/BankTransferConfirmingView.swift b/Sources/PaystackUI/Charge/BankTransfer/Views/BankTransferConfirmingView.swift index 4974da2..d832e05 100644 --- a/Sources/PaystackUI/Charge/BankTransfer/Views/BankTransferConfirmingView.swift +++ b/Sources/PaystackUI/Charge/BankTransfer/Views/BankTransferConfirmingView.swift @@ -43,7 +43,7 @@ struct BankTransferConfirmingView: View { Text(bodyCopy) .font(.body16M) - .foregroundColor(.stackBlue) + .foregroundColor(.contentPrimary) .multilineTextAlignment(.center) .animation(.easeInOut(duration: 0.25), value: phase) @@ -54,11 +54,11 @@ struct BankTransferConfirmingView: View { .disabled(true) Button("Back to account number", action: onBackToAccountNumber) - .foregroundColor(.navy02) + .foregroundColor(.contentSecondary) .font(.body14M) Button("Change payment method", action: onChangePaymentMethod) - .foregroundColor(.navy02) + .foregroundColor(.contentSecondary) .font(.body14M) } .padding(.doublePadding) @@ -92,10 +92,10 @@ struct BankTransferConfirmingView: View { GeometryReader { geo in ZStack(alignment: .leading) { Rectangle() - .fill(Color.gray01) + .fill(Color.trackInactive) .frame(height: 1) Rectangle() - .fill(Color.stackGreen) + .fill(Color.accentPrimary) .frame(width: receivedByBackend ? geo.size.width : 0, height: 1) .animation(.easeInOut(duration: 0.45), value: receivedByBackend) diff --git a/Sources/PaystackUI/Charge/BankTransfer/Views/BankTransferDelayedConfirmationView.swift b/Sources/PaystackUI/Charge/BankTransfer/Views/BankTransferDelayedConfirmationView.swift index 69c49d9..60b1bf8 100644 --- a/Sources/PaystackUI/Charge/BankTransfer/Views/BankTransferDelayedConfirmationView.swift +++ b/Sources/PaystackUI/Charge/BankTransfer/Views/BankTransferDelayedConfirmationView.swift @@ -12,12 +12,12 @@ struct BankTransferDelayedConfirmationView: View { Text("We'll complete this transaction automatically once we confirm your transfer.") .font(.body16M) - .foregroundColor(.stackBlue) + .foregroundColor(.contentPrimary) .multilineTextAlignment(.center) Text("If you have any issues with this transfer, please contact us via \(supportEmail) with the following details:") .font(.body14R) - .foregroundColor(.stackBlue) + .foregroundColor(.contentPrimary) .multilineTextAlignment(.center) VStack(alignment: .leading, spacing: .singlePadding) { @@ -33,7 +33,7 @@ struct BankTransferDelayedConfirmationView: View { .buttonStyle(SecondaryButtonStyle()) Button("Keep waiting", action: onKeepWaiting) - .foregroundColor(.navy02) + .foregroundColor(.contentSecondary) .font(.body14M) } .padding(.doublePadding) @@ -49,10 +49,10 @@ private struct NumberedListRow: View { HStack(spacing: .singlePadding) { Text("\(index).") .font(.body14R) - .foregroundColor(.stackBlue) + .foregroundColor(.contentPrimary) Text(text) .font(.body14R) - .foregroundColor(.stackBlue) + .foregroundColor(.contentPrimary) } } } diff --git a/Sources/PaystackUI/Charge/BankTransfer/Views/BankTransferRefundInitiatedView.swift b/Sources/PaystackUI/Charge/BankTransfer/Views/BankTransferRefundInitiatedView.swift index 3132e24..7f7cc43 100644 --- a/Sources/PaystackUI/Charge/BankTransfer/Views/BankTransferRefundInitiatedView.swift +++ b/Sources/PaystackUI/Charge/BankTransfer/Views/BankTransferRefundInitiatedView.swift @@ -17,12 +17,12 @@ struct BankTransferRefundInitiatedView: View { Text("Refund initiated") .font(.heading2) - .foregroundColor(.stackBlue) + .foregroundColor(.contentPrimary) .multilineTextAlignment(.center) Text(message) .font(.body14R) - .foregroundColor(.navy02) + .foregroundColor(.contentSecondary) .multilineTextAlignment(.center) .padding(.horizontal, .singlePadding) @@ -31,7 +31,7 @@ struct BankTransferRefundInitiatedView: View { .buttonStyle(PrimaryButtonStyle(showLoading: false)) Button("Close", action: dismissWithError) - .foregroundColor(.navy02) + .foregroundColor(.contentSecondary) .font(.body14M) } .padding(.doublePadding) diff --git a/Sources/PaystackUI/Charge/BankTransfer/Views/BankTransferTakingLongerView.swift b/Sources/PaystackUI/Charge/BankTransfer/Views/BankTransferTakingLongerView.swift index 5c52538..d56f9cd 100644 --- a/Sources/PaystackUI/Charge/BankTransfer/Views/BankTransferTakingLongerView.swift +++ b/Sources/PaystackUI/Charge/BankTransfer/Views/BankTransferTakingLongerView.swift @@ -14,7 +14,7 @@ struct BankTransferTakingLongerView: View { Text("It's taking longer than expected to confirm your transfer. " + "You don't have to wait here till we confirm it.") .font(.body16M) - .foregroundColor(.stackBlue) + .foregroundColor(.contentPrimary) .multilineTextAlignment(.center) timeline @@ -23,11 +23,11 @@ struct BankTransferTakingLongerView: View { .buttonStyle(PrimaryButtonStyle(showLoading: false)) Button("Back to account number", action: onBackToAccountNumber) - .foregroundColor(.navy02) + .foregroundColor(.contentSecondary) .font(.body14M) Button("Change payment method", action: onChangePaymentMethod) - .foregroundColor(.navy02) + .foregroundColor(.contentSecondary) .font(.body14M) } .padding(.doublePadding) @@ -37,7 +37,7 @@ struct BankTransferTakingLongerView: View { HStack(spacing: 0) { TimelineNode(label: "Sent", state: .complete) Rectangle() - .fill(Color.gray01) + .fill(Color.trackInactive) .frame(height: 1) .padding(.horizontal, .singlePadding) TimelineNode(label: "Received", state: .pending) diff --git a/Sources/PaystackUI/Charge/ChargeCard/Address/CardAddressView.swift b/Sources/PaystackUI/Charge/ChargeCard/Address/CardAddressView.swift index c2fa74a..bc81637 100644 --- a/Sources/PaystackUI/Charge/ChargeCard/Address/CardAddressView.swift +++ b/Sources/PaystackUI/Charge/ChargeCard/Address/CardAddressView.swift @@ -24,7 +24,7 @@ struct CardAddressView: View { Text(displayMessage) .font(.body16M) - .foregroundColor(.stackBlue) + .foregroundColor(.contentPrimary) .multilineTextAlignment(.center) FormInput(title: "Complete Payment", diff --git a/Sources/PaystackUI/Charge/ChargeCard/Birthday/CardBirthdayView.swift b/Sources/PaystackUI/Charge/ChargeCard/Birthday/CardBirthdayView.swift index 7e1878e..0ecc6b2 100644 --- a/Sources/PaystackUI/Charge/ChargeCard/Birthday/CardBirthdayView.swift +++ b/Sources/PaystackUI/Charge/ChargeCard/Birthday/CardBirthdayView.swift @@ -31,7 +31,7 @@ struct CardBirthdayView: View { Text(displayMessage) .font(.body16M) - .foregroundColor(.stackBlue) + .foregroundColor(.contentPrimary) .multilineTextAlignment(.center) FormInput(title: "Authorize", diff --git a/Sources/PaystackUI/Charge/ChargeCard/OTP/CardOTPVIew.swift b/Sources/PaystackUI/Charge/ChargeCard/OTP/CardOTPVIew.swift index 4727143..f0b82be 100644 --- a/Sources/PaystackUI/Charge/ChargeCard/OTP/CardOTPVIew.swift +++ b/Sources/PaystackUI/Charge/ChargeCard/OTP/CardOTPVIew.swift @@ -23,7 +23,7 @@ struct CardOTPVIew: View { Text(displayMessage) .font(.body16M) - .foregroundColor(.stackBlue) + .foregroundColor(.contentPrimary) .multilineTextAlignment(.center) FormInput(title: "Authorize", diff --git a/Sources/PaystackUI/Charge/ChargeCard/Phone/CardPhoneView.swift b/Sources/PaystackUI/Charge/ChargeCard/Phone/CardPhoneView.swift index 5170a71..0474200 100644 --- a/Sources/PaystackUI/Charge/ChargeCard/Phone/CardPhoneView.swift +++ b/Sources/PaystackUI/Charge/ChargeCard/Phone/CardPhoneView.swift @@ -26,7 +26,7 @@ struct CardPhoneView: View { Text(displayMessage) .font(.body16M) - .foregroundColor(.stackBlue) + .foregroundColor(.contentPrimary) .multilineTextAlignment(.center) FormInput(title: "Send OTP", diff --git a/Sources/PaystackUI/Charge/ChargeCard/Pin/CardPinView.swift b/Sources/PaystackUI/Charge/ChargeCard/Pin/CardPinView.swift index 0d07378..8e72916 100644 --- a/Sources/PaystackUI/Charge/ChargeCard/Pin/CardPinView.swift +++ b/Sources/PaystackUI/Charge/ChargeCard/Pin/CardPinView.swift @@ -24,7 +24,7 @@ struct CardPinView: View { onCommit: viewModel.submitPin) Button("Cancel", action: viewModel.cancelTransaction) - .foregroundColor(.navy02) + .foregroundColor(.contentSecondary) .font(.body14M) } #endif @@ -36,12 +36,12 @@ struct CardPinView: View { var titleText: some View { Text("Please enter your 4-digit card pin to authorize this payment") .font(.body16M) - .foregroundColor(.stackBlue) + .foregroundColor(.contentPrimary) .multilineTextAlignment(.center) } var loadingView: some View { - LoadingIndicator(tintColor: .navy04) + LoadingIndicator(tintColor: .spinnerTint) .scaleEffect(2) .padding(.doublePadding) } diff --git a/Sources/PaystackUI/Charge/ChargeCard/Redirect/CardRedirectView.swift b/Sources/PaystackUI/Charge/ChargeCard/Redirect/CardRedirectView.swift index dc88699..47f3b12 100644 --- a/Sources/PaystackUI/Charge/ChargeCard/Redirect/CardRedirectView.swift +++ b/Sources/PaystackUI/Charge/ChargeCard/Redirect/CardRedirectView.swift @@ -33,7 +33,7 @@ struct CardRedirectView: View { isLoading: $webviewLoading) if webviewLoading { - LoadingIndicator(tintColor: .navy04) + LoadingIndicator(tintColor: .spinnerTint) .scaleEffect(2) } } @@ -52,7 +52,7 @@ struct CardRedirectView: View { var titleText: some View { Text("Please click the button below to authenticate with your bank") .font(.body16M) - .foregroundColor(.stackBlue) + .foregroundColor(.contentPrimary) .multilineTextAlignment(.center) } @@ -65,7 +65,7 @@ struct CardRedirectView: View { var cancelButton: some View { Button("Cancel", action: viewModel.cancelTransaction) - .foregroundColor(.navy02) + .foregroundColor(.contentSecondary) .font(.body14M) } diff --git a/Sources/PaystackUI/Charge/ChargeCard/TestMode/TestModeCardSelectionView.swift b/Sources/PaystackUI/Charge/ChargeCard/TestMode/TestModeCardSelectionView.swift index 9c51158..f8c41d3 100644 --- a/Sources/PaystackUI/Charge/ChargeCard/TestMode/TestModeCardSelectionView.swift +++ b/Sources/PaystackUI/Charge/ChargeCard/TestMode/TestModeCardSelectionView.swift @@ -21,7 +21,7 @@ struct TestModeCardSelectionView: View { VStack(spacing: .triplePadding) { Text("Use any of the options below to test the payment flow") .font(.body16M) - .foregroundColor(.stackBlue) + .foregroundColor(.contentPrimary) .multilineTextAlignment(.center) FormInput(title: viewModel.buttonTitle, @@ -42,7 +42,7 @@ struct TestModeCardSelectionView: View { selectedItem: $viewModel.testCard) { item in HStack { Text(item.description) - .foregroundColor(.stackBlue) + .foregroundColor(.contentPrimary) .font(.body14R) Spacer() diff --git a/Sources/PaystackUI/Charge/ChargeView.swift b/Sources/PaystackUI/Charge/ChargeView.swift index 729d181..be0df9d 100644 --- a/Sources/PaystackUI/Charge/ChargeView.swift +++ b/Sources/PaystackUI/Charge/ChargeView.swift @@ -47,6 +47,8 @@ struct ChargeView: View { .aspectRatio(contentMode: .fit) .frame(width: 140) } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.surfacePrimary.ignoresSafeArea()) .task(viewModel.verifyAccessCodeAndProceed) .modalCancelButton(showConfirmation: viewModel.displayCloseButtonConfirmation, onCancelled: chargeCancelled) diff --git a/Sources/PaystackUI/Charge/MobileMoney/Views/ChannelSelectionView.swift b/Sources/PaystackUI/Charge/MobileMoney/Views/ChannelSelectionView.swift index 679fa93..7f954d6 100644 --- a/Sources/PaystackUI/Charge/MobileMoney/Views/ChannelSelectionView.swift +++ b/Sources/PaystackUI/Charge/MobileMoney/Views/ChannelSelectionView.swift @@ -23,7 +23,7 @@ struct ChannelSelectionView: View { Text("Choose a payment method to continue") .font(.body16M) - .foregroundColor(.stackBlue) + .foregroundColor(.contentPrimary) .multilineTextAlignment(.center) VStack(spacing: .singlePadding) { @@ -84,7 +84,7 @@ struct ChannelView: View { Text(channelTitle) .font(.body16M) - .foregroundColor(.stackBlue) + .foregroundColor(.contentPrimary) .lineLimit(1) .minimumScaleFactor(0.85) @@ -96,7 +96,7 @@ struct ChannelView: View { .cornerRadius(.cornerRadius) .overlay( RoundedRectangle(cornerRadius: .cornerRadius) - .stroke(Color.navy05, lineWidth: 1) + .stroke(Color.borderPrimary, lineWidth: 1) ) } } diff --git a/Sources/PaystackUI/Charge/MobileMoney/Views/MobileMoneyChargeView.swift b/Sources/PaystackUI/Charge/MobileMoney/Views/MobileMoneyChargeView.swift index bf05e5a..8c3b6e9 100644 --- a/Sources/PaystackUI/Charge/MobileMoney/Views/MobileMoneyChargeView.swift +++ b/Sources/PaystackUI/Charge/MobileMoney/Views/MobileMoneyChargeView.swift @@ -39,7 +39,7 @@ struct MobileMoneyChargeView: View { Text("Please enter the mobile money number to begin this payment") .font(.body16M) - .foregroundColor(.stackBlue) + .foregroundColor(.contentPrimary) .multilineTextAlignment(.center) FormInput(title: "Pay \(viewModel.transactionDetails.amountCurrency.description)", diff --git a/Sources/PaystackUI/Charge/MobileMoney/Views/MobileMoneyProcessingView.swift b/Sources/PaystackUI/Charge/MobileMoney/Views/MobileMoneyProcessingView.swift index efd726f..a8b17ab 100644 --- a/Sources/PaystackUI/Charge/MobileMoney/Views/MobileMoneyProcessingView.swift +++ b/Sources/PaystackUI/Charge/MobileMoney/Views/MobileMoneyProcessingView.swift @@ -20,12 +20,12 @@ struct MobileMoneyProcessingView: View { Image.otpIcon Text(viewModel.mobileMoneyTransaction.phone) .font(.body16M) - .foregroundColor(.stackBlue) + .foregroundColor(.contentPrimary) .multilineTextAlignment(.center) Text(viewModel.authorizationPromptText) .font(.body16M) - .foregroundColor(.stackBlue) + .foregroundColor(.contentPrimary) .multilineTextAlignment(.center) CountdownView(counter: $viewModel.counter, @@ -63,7 +63,7 @@ struct ProgressTrack: View { .fill(Color.clear) .frame(width: 35, height: 35) .overlay( - Circle().stroke(Color.gray01, lineWidth: 5) + Circle().stroke(Color.trackInactive, lineWidth: 5) ) } } @@ -86,7 +86,7 @@ struct ProgressBar: View { ) ) .foregroundColor( - (completed() ? Color.stackGreen : Color.stackGreen) + (completed() ? Color.accentPrimary : Color.accentPrimary) ).animation( .easeInOut(duration: 0.2) ) @@ -115,7 +115,7 @@ struct CountdownView: View { } Text("Payment is valid for \(counterToMinutes())") .font(.body16M) - .foregroundColor(.stackBlue) + .foregroundColor(.contentPrimary) .multilineTextAlignment(.center) }.onReceive(timer) { _ in if self.counter < self.countTo { diff --git a/Sources/PaystackUI/Charge/Success/ChargeSuccessView.swift b/Sources/PaystackUI/Charge/Success/ChargeSuccessView.swift index c17babe..4a714d5 100644 --- a/Sources/PaystackUI/Charge/Success/ChargeSuccessView.swift +++ b/Sources/PaystackUI/Charge/Success/ChargeSuccessView.swift @@ -16,11 +16,11 @@ struct ChargeSuccessView: View { Text("Payment Successful") .font(.heading3) - .foregroundColor(.stackBlue) + .foregroundColor(.contentPrimary) Text("You paid \(amount.description) to \(merchant)") .font(.body16R) - .foregroundColor(.navy03) + .foregroundColor(.contentTertiary) } .task(dismissAutomatically) } diff --git a/Sources/PaystackUI/Charge/Zap/Components/QRCodeImage.swift b/Sources/PaystackUI/Charge/Zap/Components/QRCodeImage.swift index b3be795..5016d80 100644 --- a/Sources/PaystackUI/Charge/Zap/Components/QRCodeImage.swift +++ b/Sources/PaystackUI/Charge/Zap/Components/QRCodeImage.swift @@ -25,7 +25,7 @@ struct QRCodeImage: View { .transition(.opacity) } else if loadFailed { Image(systemName: "exclamationmark.triangle") - .foregroundColor(.warning02) + .foregroundColor(.contentWarning) } else { ShimmerPlaceholder() } @@ -66,12 +66,12 @@ struct ShimmerPlaceholder: View { var body: some View { GeometryReader { geo in ZStack { - Color.gray01 + Color.shimmerBase LinearGradient( gradient: Gradient(stops: [ - .init(color: Color.white.opacity(0.0), location: 0.0), - .init(color: Color.white.opacity(0.5), location: 0.5), - .init(color: Color.white.opacity(0.0), location: 1.0) + .init(color: Color.shimmerHighlight.opacity(0.0), location: 0.0), + .init(color: Color.shimmerHighlight.opacity(0.5), location: 0.5), + .init(color: Color.shimmerHighlight.opacity(0.0), location: 1.0) ]), startPoint: .topLeading, endPoint: .bottomTrailing) diff --git a/Sources/PaystackUI/Charge/Zap/Views/ZapPaymentView.swift b/Sources/PaystackUI/Charge/Zap/Views/ZapPaymentView.swift index 566bb1e..4401019 100644 --- a/Sources/PaystackUI/Charge/Zap/Views/ZapPaymentView.swift +++ b/Sources/PaystackUI/Charge/Zap/Views/ZapPaymentView.swift @@ -20,7 +20,7 @@ struct ZapPaymentView: View { } private var countdownValueColor: Color { - remainingSeconds <= 60 ? .warning02 : .stackGreen + remainingSeconds <= 60 ? .contentWarning : .accentPrimary } var body: some View { @@ -31,7 +31,7 @@ struct ZapPaymentView: View { Text("Open Zap or scan this QR code to complete this payment") .font(.body16M) - .foregroundColor(.stackBlue) + .foregroundColor(.contentPrimary) .multilineTextAlignment(.center) qrCodeBlock @@ -58,17 +58,17 @@ struct ZapPaymentView: View { QRCodeImage(url: details.qrImageURL) .frame(width: 220, height: 220) .padding(.singlePadding) - .background(Color.white) + .background(Color.qrPlate) .overlay( RoundedRectangle(cornerRadius: .cornerRadius) - .stroke(Color.navy05, lineWidth: 1)) + .stroke(Color.borderPrimary, lineWidth: 1)) } private var expiresInRow: some View { HStack(spacing: 4) { Text("Expires in") .font(.body14M) - .foregroundColor(.navy03) + .foregroundColor(.contentTertiary) Text(formattedRemaining) .font(.body14M) .foregroundColor(countdownValueColor) @@ -84,7 +84,7 @@ struct ZapPaymentView: View { } Button("Change payment method", action: onChangePaymentMethod) - .foregroundColor(.navy02) + .foregroundColor(.contentSecondary) .font(.body14M) .padding(.top, .singlePadding) } diff --git a/Sources/PaystackUI/Charge/Zap/Views/ZapSessionExpiredView.swift b/Sources/PaystackUI/Charge/Zap/Views/ZapSessionExpiredView.swift index ec247ad..c677514 100644 --- a/Sources/PaystackUI/Charge/Zap/Views/ZapSessionExpiredView.swift +++ b/Sources/PaystackUI/Charge/Zap/Views/ZapSessionExpiredView.swift @@ -13,12 +13,12 @@ struct ZapSessionExpiredView: View { Text("Session expired") .font(.heading2) - .foregroundColor(.stackBlue) + .foregroundColor(.contentPrimary) .multilineTextAlignment(.center) Text(message) .font(.body14R) - .foregroundColor(.navy02) + .foregroundColor(.contentSecondary) .multilineTextAlignment(.center) .padding(.horizontal, .singlePadding) diff --git a/Sources/PaystackUI/Components/AccountDetailRow.swift b/Sources/PaystackUI/Components/AccountDetailRow.swift index d6045cc..8af47c1 100644 --- a/Sources/PaystackUI/Components/AccountDetailRow.swift +++ b/Sources/PaystackUI/Components/AccountDetailRow.swift @@ -26,11 +26,11 @@ struct AccountDetailRow: View { VStack(alignment: .leading, spacing: .quarterPadding) { Text(label) .font(.body12M) - .foregroundColor(.navy03) + .foregroundColor(.contentTertiary) .padding(.bottom, .singlePadding) Text(value) .font(.body16M) - .foregroundColor(.stackBlue) + .foregroundColor(.contentPrimary) } Spacer() trailingView @@ -47,7 +47,7 @@ struct AccountDetailRow: View { #if os(iOS) Button(action: copy) { Image(systemName: justCopied ? "checkmark" : "doc.on.doc") - .foregroundColor(justCopied ? .stackGreen : .navy02) + .foregroundColor(justCopied ? .accentPrimary : .contentSecondary) } #else EmptyView() @@ -55,7 +55,7 @@ struct AccountDetailRow: View { case .text(let title, let action): Button(title, action: action) .font(.body14M) - .foregroundColor(.stackGreen) + .foregroundColor(.accentPrimary) } } diff --git a/Sources/PaystackUI/Components/ChangePaymentMethodFooter.swift b/Sources/PaystackUI/Components/ChangePaymentMethodFooter.swift index 83e7ac6..0880944 100644 --- a/Sources/PaystackUI/Components/ChangePaymentMethodFooter.swift +++ b/Sources/PaystackUI/Components/ChangePaymentMethodFooter.swift @@ -12,11 +12,11 @@ struct ChangePaymentMethodFooter: View { Text("Change payment method") } .font(.body14M) - .foregroundColor(.navy02) + .foregroundColor(.contentSecondary) .padding(.horizontal, .doublePadding) .padding(.vertical, .singlePadding) .overlay( - Capsule().stroke(Color.navy05, lineWidth: 1) + Capsule().stroke(Color.borderPrimary, lineWidth: 1) ) } .padding(.bottom, .doublePadding) diff --git a/Sources/PaystackUI/Components/ErrorView.swift b/Sources/PaystackUI/Components/ErrorView.swift index fbdd7f8..a2bc341 100644 --- a/Sources/PaystackUI/Components/ErrorView.swift +++ b/Sources/PaystackUI/Components/ErrorView.swift @@ -27,7 +27,7 @@ struct ErrorView: View { Text(message) .font(.body16M) - .foregroundColor(.stackBlue) + .foregroundColor(.contentPrimary) .padding(.bottom, .singlePadding) if let buttonText, let buttonAction { diff --git a/Sources/PaystackUI/Components/Form/FormInput.swift b/Sources/PaystackUI/Components/Form/FormInput.swift index 874c132..1b3d4c0 100644 --- a/Sources/PaystackUI/Components/Form/FormInput.swift +++ b/Sources/PaystackUI/Components/Form/FormInput.swift @@ -66,7 +66,7 @@ struct FormInput) -> some View { configuration .padding(.doublePadding) - .foregroundColor(.stackBlue) + .foregroundColor(.contentPrimary) .font(.body16R) } } diff --git a/Sources/PaystackUI/Components/Form/PickerFormInputView.swift b/Sources/PaystackUI/Components/Form/PickerFormInputView.swift index 23afa75..77e0b7c 100644 --- a/Sources/PaystackUI/Components/Form/PickerFormInputView.swift +++ b/Sources/PaystackUI/Components/Form/PickerFormInputView.swift @@ -38,7 +38,7 @@ struct PickerFormInputView: FormInputI .frame(maxWidth: .infinity) .background( RoundedRectangle(cornerRadius: .cornerRadius, style: .continuous) - .stroke(Color.navy04, lineWidth: 1) + .stroke(Color.borderStrong, lineWidth: 1) ) .form(title) } @@ -47,10 +47,10 @@ struct PickerFormInputView: FormInputI HStack { if let selectedItem = selectedItem { Text(selectedItem.description) - .foregroundColor(.stackBlue) + .foregroundColor(.contentPrimary) } else { Text(placeholder) - .foregroundColor(.navy04) + .foregroundColor(.contentPlaceholder) } Spacer() diff --git a/Sources/PaystackUI/Components/Form/TextFieldFocusedBorder.swift b/Sources/PaystackUI/Components/Form/TextFieldFocusedBorder.swift index 711d239..b03aa36 100644 --- a/Sources/PaystackUI/Components/Form/TextFieldFocusedBorder.swift +++ b/Sources/PaystackUI/Components/Form/TextFieldFocusedBorder.swift @@ -13,7 +13,7 @@ struct TextFieldFocusedBorder: ViewModifier { .focused($isFocused) .background( RoundedRectangle(cornerRadius: .cornerRadius, style: .continuous) - .stroke(isFocused ? Color.stackGreen : defaultColor, lineWidth: 1) + .stroke(isFocused ? Color.accentPrimary : defaultColor, lineWidth: 1) ) } } @@ -21,7 +21,7 @@ struct TextFieldFocusedBorder: ViewModifier { extension View { @ViewBuilder - func focusedBorderColor(defaultColor: Color = .navy04) -> some View { + func focusedBorderColor(defaultColor: Color = .borderStrong) -> some View { if #available(iOS 15, macOS 12.0, *) { self.modifier(TextFieldFocusedBorder(defaultColor: defaultColor)) } else { diff --git a/Sources/PaystackUI/Components/Form/TextFieldFormInputView.swift b/Sources/PaystackUI/Components/Form/TextFieldFormInputView.swift index 92e6f57..c5bc742 100644 --- a/Sources/PaystackUI/Components/Form/TextFieldFormInputView.swift +++ b/Sources/PaystackUI/Components/Form/TextFieldFormInputView.swift @@ -62,7 +62,7 @@ struct TextFieldFormInputView: FormInputItemView { } } .textFieldStyle(.form) - .focusedBorderColor(defaultColor: inErrorState ? .error02 : .navy04) + .focusedBorderColor(defaultColor: inErrorState ? .borderError : .borderStrong) .setDefaultFocus(defaultFocused) .keyboardType(keyboardType) .onReceive(Just(text)) { value in diff --git a/Sources/PaystackUI/Components/Form/TitleFormInputView.swift b/Sources/PaystackUI/Components/Form/TitleFormInputView.swift index 4d8ddb0..35d916d 100644 --- a/Sources/PaystackUI/Components/Form/TitleFormInputView.swift +++ b/Sources/PaystackUI/Components/Form/TitleFormInputView.swift @@ -10,7 +10,7 @@ struct TitleFormInputView: FormInputItemViewModifier { var body: some View { VStack(alignment: .leading, spacing: .halfPadding) { Text(title) - .foregroundColor(.navy02) + .foregroundColor(.contentSecondary) .font(.body14R) content diff --git a/Sources/PaystackUI/Components/Form/Validation/ValidationFormInputView.swift b/Sources/PaystackUI/Components/Form/Validation/ValidationFormInputView.swift index eeaa33c..aace4f7 100644 --- a/Sources/PaystackUI/Components/Form/Validation/ValidationFormInputView.swift +++ b/Sources/PaystackUI/Components/Form/Validation/ValidationFormInputView.swift @@ -37,7 +37,7 @@ struct ValidationFormInputView: FormInputItemViewMod if !validatorStates[index] { Text(validator.errorMessage) .font(.body12R) - .foregroundColor(.error01) + .foregroundColor(.contentError) } } } diff --git a/Sources/PaystackUI/Components/LoadingIndicator.swift b/Sources/PaystackUI/Components/LoadingIndicator.swift index 449950a..92e66ca 100644 --- a/Sources/PaystackUI/Components/LoadingIndicator.swift +++ b/Sources/PaystackUI/Components/LoadingIndicator.swift @@ -5,7 +5,7 @@ struct LoadingIndicator: View { var tintColor: Color - init(tintColor: Color = .white) { + init(tintColor: Color = .contentOnAccent) { self.tintColor = tintColor } diff --git a/Sources/PaystackUI/Components/LoadingView.swift b/Sources/PaystackUI/Components/LoadingView.swift index 99cc3e8..dad82b1 100644 --- a/Sources/PaystackUI/Components/LoadingView.swift +++ b/Sources/PaystackUI/Components/LoadingView.swift @@ -17,9 +17,9 @@ struct LoadingView: View { VStack(spacing: .quadPadding) { Text(message) .font(.body16M) - .foregroundColor(.stackBlue) + .foregroundColor(.contentPrimary) - LoadingIndicator(tintColor: .navy04) + LoadingIndicator(tintColor: .spinnerTint) .scaleEffect(2) .padding(.bottom, .triplePadding) } diff --git a/Sources/PaystackUI/Components/ModalCancelButton.swift b/Sources/PaystackUI/Components/ModalCancelButton.swift index 2571690..9ccc5eb 100644 --- a/Sources/PaystackUI/Components/ModalCancelButton.swift +++ b/Sources/PaystackUI/Components/ModalCancelButton.swift @@ -35,7 +35,7 @@ struct ModalCancelButton: ViewModifier { Button(action: closeButtonTapped) { Image(systemName: "xmark") .font(.system(size: 20, weight: .medium)) - .foregroundColor(.gray01) + .foregroundColor(.iconMuted) } } diff --git a/Sources/PaystackUI/Components/PinTextView/SwiftUI/Core/PinTextView.swift b/Sources/PaystackUI/Components/PinTextView/SwiftUI/Core/PinTextView.swift index 916770f..1162296 100644 --- a/Sources/PaystackUI/Components/PinTextView/SwiftUI/Core/PinTextView.swift +++ b/Sources/PaystackUI/Components/PinTextView/SwiftUI/Core/PinTextView.swift @@ -78,11 +78,11 @@ struct PinTextView: View { otpBackgroundColor: UIColor = .clear, otpFilledBackgroundColor: UIColor = .clear, otpCornerRaduis: CGFloat = 3, - otpDefaultBorderColor: UIColor = UIColor(red: 0.89, green: 0.89, blue: 0.89, alpha: 1), - otpFilledBorderColor: UIColor = UIColor(.stackGreen), + otpDefaultBorderColor: UIColor = .pinSlotBorder, + otpFilledBorderColor: UIColor = .accentPrimary, otpDefaultBorderWidth: CGFloat = 1, otpFilledBorderWidth: CGFloat = 1, - otpTextColor: UIColor = UIColor(.stackBlue), + otpTextColor: UIColor = .contentPrimary, otpFontSize: CGFloat = 18, otpFont: UIFont = .heading2, isSecureTextEntry: Bool = false, diff --git a/Sources/PaystackUI/Components/PinTextView/SwiftUI/Core/PinTextViewRepresentable.swift b/Sources/PaystackUI/Components/PinTextView/SwiftUI/Core/PinTextViewRepresentable.swift index 8d2b6d4..921767a 100644 --- a/Sources/PaystackUI/Components/PinTextView/SwiftUI/Core/PinTextViewRepresentable.swift +++ b/Sources/PaystackUI/Components/PinTextView/SwiftUI/Core/PinTextViewRepresentable.swift @@ -25,8 +25,8 @@ struct PinTextViewRepresentable: UIViewRepresentable { text: Binding, slotsCount: Int = 4, otpDefaultCharacter: String = "", - otpBackgroundColor: UIColor = UIColor(red: 0.949, green: 0.949, blue: 0.949, alpha: 1), - otpFilledBackgroundColor: UIColor = UIColor(red: 0.949, green: 0.949, blue: 0.949, alpha: 1), + otpBackgroundColor: UIColor = .pinSlotSurface, + otpFilledBackgroundColor: UIColor = .pinSlotSurface, otpCornerRaduis: CGFloat = 3, otpDefaultBorderColor: UIColor = .gray, otpFilledBorderColor: UIColor = .green, diff --git a/Sources/PaystackUI/Components/PinTextView/UIKit/Core/PinTextField.swift b/Sources/PaystackUI/Components/PinTextView/UIKit/Core/PinTextField.swift index d035a2a..7c7d482 100644 --- a/Sources/PaystackUI/Components/PinTextView/UIKit/Core/PinTextField.swift +++ b/Sources/PaystackUI/Components/PinTextView/UIKit/Core/PinTextField.swift @@ -9,21 +9,21 @@ class PinTextField: UITextField { /// The default character placed in the text field slots var otpDefaultCharacter = "" /// The default background color of the text field slots before entering a character - var otpBackgroundColor: UIColor = UIColor(red: 0.949, green: 0.949, blue: 0.949, alpha: 1) + var otpBackgroundColor: UIColor = .pinSlotSurface /// The default background color of the text field slots after entering a character - var otpFilledBackgroundColor: UIColor = UIColor(red: 0.949, green: 0.949, blue: 0.949, alpha: 1) + var otpFilledBackgroundColor: UIColor = .pinSlotSurface /// The default corner raduis of the text field slots var otpCornerRaduis: CGFloat = 3 /// The default border color of the text field slots before entering a character - var otpDefaultBorderColor: UIColor = .gray + var otpDefaultBorderColor: UIColor = .pinSlotBorder /// The border color of the text field slots after entering a character - var otpFilledBorderColor: UIColor = .green + var otpFilledBorderColor: UIColor = .accentPrimary /// The default border width of the text field slots before entering a character var otpDefaultBorderWidth: CGFloat = 1 /// The border width of the text field slots after entering a character var otpFilledBorderWidth: CGFloat = 1 /// The default text color of the text - var otpTextColor: UIColor = .black + var otpTextColor: UIColor = .contentPrimary /// The default font size of the text var otpFontSize: CGFloat = 18 /// The default font of the text diff --git a/Sources/PaystackUI/Components/Styles/PrimaryButtonStyle.swift b/Sources/PaystackUI/Components/Styles/PrimaryButtonStyle.swift index eae42c0..be5eb41 100644 --- a/Sources/PaystackUI/Components/Styles/PrimaryButtonStyle.swift +++ b/Sources/PaystackUI/Components/Styles/PrimaryButtonStyle.swift @@ -36,19 +36,19 @@ struct PrimaryButtonStyle: ButtonStyle { extension PrimaryButtonStyle { var background: Color { - isEnabled ? .stackGreen : .stackGreen.opacity(0.6) + isEnabled ? .accentPrimary : .accentPrimaryDisabled } var pressedBackground: Color { - .stackGreen.opacity(0.75) + .accentPrimaryPressed } var foreground: Color { - .white + .contentOnAccent } var pressedForeground: Color { - .white.opacity(0.75) + .contentOnAccentPressed } } @@ -85,7 +85,7 @@ struct PaymentChannelSelectionButton: ButtonStyle { .padding() .frame(height: .buttonHeight) .frame(maxWidth: .infinity) - .focusedBorderColor(defaultColor: .gray01) + .focusedBorderColor(defaultColor: .borderSubtle) .font(.body16M) .background(configuration.isPressed ? pressedBackground : background) .foregroundColor(configuration.isPressed ? pressedForeground : foreground) @@ -99,19 +99,19 @@ struct PaymentChannelSelectionButton: ButtonStyle { extension PaymentChannelSelectionButton { var background: Color { - isEnabled ? .stackGreen : .stackGreen.opacity(0.6) + isEnabled ? .accentPrimary : .accentPrimaryDisabled } var pressedBackground: Color { - .stackGreen.opacity(0.75) + .accentPrimaryPressed } var foreground: Color { - .white + .contentOnAccent } var pressedForeground: Color { - .white.opacity(0.75) + .contentOnAccentPressed } } diff --git a/Sources/PaystackUI/Components/Styles/SecondaryButtonStyle.swift b/Sources/PaystackUI/Components/Styles/SecondaryButtonStyle.swift index 7e8b7de..747c392 100644 --- a/Sources/PaystackUI/Components/Styles/SecondaryButtonStyle.swift +++ b/Sources/PaystackUI/Components/Styles/SecondaryButtonStyle.swift @@ -38,19 +38,19 @@ struct SecondaryButtonStyle: ButtonStyle { extension SecondaryButtonStyle { var border: Color { - .green02 + .accentSecondary } var pressedBorder: Color { - .green02 + .accentSecondary } var foreground: Color { - .green02 + .accentSecondary } var pressedForeground: Color { - .green02 + .accentSecondary } } diff --git a/Sources/PaystackUI/Components/TestModeInidcator.swift b/Sources/PaystackUI/Components/TestModeInidcator.swift index 293db04..a52fb2d 100644 --- a/Sources/PaystackUI/Components/TestModeInidcator.swift +++ b/Sources/PaystackUI/Components/TestModeInidcator.swift @@ -4,14 +4,14 @@ struct TestModeInidcator: View { var body: some View { Text("TEST") - .foregroundColor(.warning01) + .foregroundColor(.testModeContent) .font(.smallTextM) .padding(.horizontal, .singlePadding + .halfPadding) .padding(.vertical, .halfPadding) - .background(Color.warning05) + .background(Color.testModeSurface) .overlay( RoundedRectangle(cornerRadius: 3) - .stroke(Color.warning04, lineWidth: 1) + .stroke(Color.testModeBorder, lineWidth: 1) ) } diff --git a/Sources/PaystackUI/Components/TimelineNode.swift b/Sources/PaystackUI/Components/TimelineNode.swift index fc1f552..bd2bd01 100644 --- a/Sources/PaystackUI/Components/TimelineNode.swift +++ b/Sources/PaystackUI/Components/TimelineNode.swift @@ -22,7 +22,7 @@ struct TimelineNode: View { VStack(spacing: .singlePadding) { ZStack { Circle() - .stroke(Color.stackGreen, lineWidth: 2) + .stroke(Color.accentPrimary, lineWidth: 2) .frame(width: diameter, height: diameter) .scaleEffect(rippleScale) .opacity(rippleOpacity) @@ -30,23 +30,23 @@ struct TimelineNode: View { switch state { case .complete: Circle() - .fill(Color.stackGreen) + .fill(Color.accentPrimary) .frame(width: diameter, height: diameter) .scaleEffect(nodeScale) Image(systemName: "checkmark") - .foregroundColor(.white) + .foregroundColor(.contentOnAccent) .font(.system(size: 14, weight: .bold)) .scaleEffect(checkmarkScale) case .pending: Circle() .stroke(style: StrokeStyle(lineWidth: 2, dash: [3, 3])) - .foregroundColor(.gray01) + .foregroundColor(.iconMuted) .frame(width: diameter, height: diameter) } } Text(label) .font(.body12M) - .foregroundColor(state == .complete ? .stackGreen : .navy03) + .foregroundColor(state == .complete ? .accentPrimary : .contentTertiary) .animation(.easeInOut(duration: 0.2), value: state) } .onChange(of: state) { newState in diff --git a/Sources/PaystackUI/Components/Views/SingleSelectionFormInputView.swift b/Sources/PaystackUI/Components/Views/SingleSelectionFormInputView.swift index bde301b..2d38973 100644 --- a/Sources/PaystackUI/Components/Views/SingleSelectionFormInputView.swift +++ b/Sources/PaystackUI/Components/Views/SingleSelectionFormInputView.swift @@ -40,7 +40,7 @@ struct SingleSelectionFormInputView Color { return Color(name, bundle: .current) } } + +#if canImport(UIKit) +import UIKit + +// MARK: - UIKit dynamic-colour bridge +// +// PIN entry uses UIKit primitives. Direct `UIColor(_: Color)` snapshots the +// SwiftUI colour against the current trait collection at init and does not +// track a later `.light` → `.dark` change. Using `UIColor(named:in:compatibleWith:)` +// with `compatibleWith: nil` resolves against the view's live trait +// collection, so appearance flips mid-session are honoured. +extension UIColor { + + static var pinSlotSurface: UIColor { + .paystackNamed("PinSlotSurface") + } + + static var pinSlotBorder: UIColor { + .paystackNamed("PinSlotBorder") + } + + static var accentPrimary: UIColor { + .paystackNamed("AccentPrimary") + } + + static var contentPrimary: UIColor { + .paystackNamed("ContentPrimary") + } + + fileprivate static func paystackNamed(_ name: String) -> UIColor { + return UIColor(named: name, in: .current, compatibleWith: nil) + ?? .clear + } +} +#endif diff --git a/Sources/PaystackUI/Design/DarkModePreviews.swift b/Sources/PaystackUI/Design/DarkModePreviews.swift new file mode 100644 index 0000000..ef65a42 --- /dev/null +++ b/Sources/PaystackUI/Design/DarkModePreviews.swift @@ -0,0 +1,222 @@ +import SwiftUI + +#if DEBUG + +@available(iOS 14.0, *) +struct DarkModeSwatchGrid: View { + + let title: String + let entries: [SwatchEntry] + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + Text(title) + .font(.body16M) + .foregroundColor(.contentPrimary) + + VStack(spacing: 6) { + ForEach(entries) { entry in + SwatchRow(entry: entry) + } + } + } + .padding(16) + } +} + +@available(iOS 14.0, *) +struct SwatchEntry: Identifiable { + let id: String + let color: Color + let notes: String? + + init(_ id: String, _ color: Color, notes: String? = nil) { + self.id = id + self.color = color + self.notes = notes + } +} + +@available(iOS 14.0, *) +private struct SwatchRow: View { + + let entry: SwatchEntry + + var body: some View { + HStack(spacing: 12) { + RoundedRectangle(cornerRadius: 6) + .fill(entry.color) + .frame(width: 40, height: 28) + .overlay( + RoundedRectangle(cornerRadius: 6) + .stroke(Color.gray.opacity(0.3), lineWidth: 1)) + + VStack(alignment: .leading, spacing: 2) { + Text(entry.id) + .font(.body14M) + .foregroundColor(.contentPrimary) + if let notes = entry.notes { + Text(notes) + .font(.body12R) + .foregroundColor(.contentSecondary) + } + } + Spacer(minLength: 0) + } + } +} + +@available(iOS 14.0, *) +enum DarkModePalette { + + static let primary: [SwatchEntry] = [ + .init("stackBlue", .stackBlue, notes: "#011B33 — content primary today"), + .init("navy02", .navy02, notes: "#596A7A — content secondary today"), + .init("navy03", .navy03, notes: "#98A6AD — content tertiary today"), + .init("navy04", .navy04, notes: "#CCD1D6 — border strong / placeholder"), + .init("navy05", .navy05, notes: "#E6E8EB — border subtle / surface secondary") + ] + + static let secondary: [SwatchEntry] = [ + .init("stackGreen", .stackGreen, notes: "#3BB75E — accent primary"), + .init("green02", .green02, notes: "#1D853A — accent secondary (fails 4.5:1 dark)") + ] + + static let error: [SwatchEntry] = [ + .init("error01", .error01, notes: "#880202 — content error (fails dark)"), + .init("error02", .error02, notes: "#D44141 — border error") + ] + + static let warning: [SwatchEntry] = [ + .init("warning01", .warning01, notes: "#673700 — test-mode content"), + .init("warning02", .warning02, notes: "#FFAA22 — content warning / countdown"), + .init("warning04", .warning04, notes: "#FFE4B8 — test-mode border"), + .init("warning05", .warning05, notes: "#FFF6DF — test-mode surface") + ] + + static let neutral: [SwatchEntry] = [ + .init("gray01", .gray01, notes: "#999DA1 — muted icon / track / shimmer base") + ] + + static let semanticSurfaces: [SwatchEntry] = [ + .init("surfacePrimary", .surfacePrimary, notes: "Sheet root"), + .init("surfaceSecondary", .surfaceSecondary, notes: "Filled inset chip"), + .init("surfaceTertiary", .surfaceTertiary, notes: "Deepest surface (reserved)"), + .init("surfaceInsetTranslucent", .surfaceInsetTranslucent, notes: "Capitec stepsCard"), + .init("overlayScrim", .overlayScrim, notes: "Reserved — no current site") + ] + + static let semanticBorders: [SwatchEntry] = [ + .init("borderPrimary", .borderPrimary, notes: "Hairline on cards"), + .init("borderSubtle", .borderSubtle, notes: "Channel row"), + .init("borderStrong", .borderStrong, notes: "Input / picker"), + .init("borderError", .borderError, notes: "Field error border") + ] + + static let semanticContent: [SwatchEntry] = [ + .init("contentPrimary", .contentPrimary, notes: "Body / heading text"), + .init("contentSecondary", .contentSecondary, notes: "Secondary body"), + .init("contentTertiary", .contentTertiary, notes: "Caption"), + .init("contentPlaceholder", .contentPlaceholder, notes: "Placeholder"), + .init("contentWarning", .contentWarning, notes: "Warning / countdown"), + .init("contentError", .contentError, notes: "Validation message"), + .init("contentOnAccent", .contentOnAccent, notes: "Label on green CTA"), + .init("contentOnAccentPressed", .contentOnAccentPressed, notes: "Pressed label") + ] + + static let semanticAccents: [SwatchEntry] = [ + .init("accentPrimary", .accentPrimary, notes: "Green CTA / focus ring"), + .init("accentPrimaryDisabled", .accentPrimaryDisabled, notes: "CTA disabled"), + .init("accentPrimaryPressed", .accentPrimaryPressed, notes: "CTA pressed"), + .init("accentSecondary", .accentSecondary, notes: "Outline button text") + ] + + static let semanticAuxiliaries: [SwatchEntry] = [ + .init("iconMuted", .iconMuted, notes: "Muted glyph"), + .init("spinnerTint", .spinnerTint, notes: "Spinner"), + .init("trackInactive", .trackInactive, notes: "Progress track") + ] + + static let semanticWarningInfoTest: [SwatchEntry] = [ + .init("warningSurface", .warningSurface, notes: "Warning banner"), + .init("infoSurface", .infoSurface, notes: "Info banner"), + .init("toastSurface", .toastSurface, notes: "Toast pill"), + .init("testModeContent", .testModeContent, notes: "Test-mode chip text"), + .init("testModeSurface", .testModeSurface, notes: "Test-mode chip fill"), + .init("testModeBorder", .testModeBorder, notes: "Test-mode chip border") + ] + + static let semanticFixed: [SwatchEntry] = [ + .init("qrPlate", .qrPlate, notes: "QR quiet-zone — must not invert"), + .init("shimmerBase", .shimmerBase, notes: "QR loader base"), + .init("shimmerHighlight", .shimmerHighlight, notes: "QR loader gloss"), + .init("pinSlotSurface", .pinSlotSurface, notes: "PIN slot fill"), + .init("pinSlotBorder", .pinSlotBorder, notes: "PIN slot border") + ] +} + +@available(iOS 14.0, *) +struct DarkModePreviews_Previews: PreviewProvider { + + static var previews: some View { + Group { + allSwatches + .previewDisplayName("All tokens — Light") + .preferredColorScheme(.light) + + allSwatches + .background(Color.surfacePrimary) + .previewDisplayName("All tokens — Dark") + .preferredColorScheme(.dark) + } + } + + private static var allSwatches: some View { + ScrollView { + VStack(alignment: .leading, spacing: 24) { + + Text("Semantic tokens (target layer)") + .font(.heading2) + .foregroundColor(.contentPrimary) + .padding(.horizontal, 16) + + DarkModeSwatchGrid(title: "Surfaces", + entries: DarkModePalette.semanticSurfaces) + DarkModeSwatchGrid(title: "Borders", + entries: DarkModePalette.semanticBorders) + DarkModeSwatchGrid(title: "Content", + entries: DarkModePalette.semanticContent) + DarkModeSwatchGrid(title: "Accents", + entries: DarkModePalette.semanticAccents) + DarkModeSwatchGrid(title: "Auxiliaries", + entries: DarkModePalette.semanticAuxiliaries) + DarkModeSwatchGrid(title: "Warning / info / test-mode / toast", + entries: DarkModePalette.semanticWarningInfoTest) + DarkModeSwatchGrid(title: "Fixed / shimmer / PIN", + entries: DarkModePalette.semanticFixed) + + Divider() + .padding(.horizontal, 16) + + Text("Hue tokens (kept as internal primitives per Q8)") + .font(.heading2) + .foregroundColor(.contentPrimary) + .padding(.horizontal, 16) + + DarkModeSwatchGrid(title: "Primary", + entries: DarkModePalette.primary) + DarkModeSwatchGrid(title: "Secondary", + entries: DarkModePalette.secondary) + DarkModeSwatchGrid(title: "Error", + entries: DarkModePalette.error) + DarkModeSwatchGrid(title: "Warning", + entries: DarkModePalette.warning) + DarkModeSwatchGrid(title: "Neutral", + entries: DarkModePalette.neutral) + } + .padding(.vertical, 16) + } + } +} + +#endif diff --git a/Sources/PaystackUI/Images/Images.swift b/Sources/PaystackUI/Images/Images.swift index 7cdf492..30b3863 100644 --- a/Sources/PaystackUI/Images/Images.swift +++ b/Sources/PaystackUI/Images/Images.swift @@ -94,13 +94,13 @@ extension Image { static var dropDownIndicator: some View { Image(systemName: "arrowtriangle.down.circle.fill") - .foregroundColor(.gray01) + .foregroundColor(.iconMuted) } static var errorIcon: some View { Image(systemName: "exclamationmark.triangle.fill") .resizable() - .foregroundColor(.warning02) + .foregroundColor(.contentWarning) .aspectRatio(contentMode: .fit) .frame(width: .quadPadding) } @@ -108,7 +108,7 @@ extension Image { static var radioButtonUnselected: some View { Image(systemName: "circle") .resizable() - .foregroundColor(.black) + .foregroundColor(.contentTertiary) .aspectRatio(contentMode: .fit) .frame(width: .doublePadding) } @@ -116,7 +116,7 @@ extension Image { static var radioButtonSelected: some View { Image(systemName: "checkmark.circle.fill") .resizable() - .foregroundColor(.stackGreen) + .foregroundColor(.accentPrimary) .aspectRatio(contentMode: .fit) .frame(width: .doublePadding) }