From 5e5d3bb22f3bae38c2bb6d449ef695a03b46ff77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andy=20Gr=C3=BCning?= Date: Wed, 10 Jun 2026 12:19:28 +0200 Subject: [PATCH 1/2] read-only walletId and walletAddress, verifying demo app in ci --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- .github/workflows/ci.yml | 3 +++ AGENTS.md | 6 +++--- API.md | 4 ++-- Sources/Swift SDK/Clients/WalletClient.swift | 4 ++-- TESTING.md | 2 +- 6 files changed, 12 insertions(+), 9 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index eefdda0..ae98ac4 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -15,7 +15,7 @@ - [ ] `swift build` passes - [ ] `swift test` passes -- [ ] Demo app builds (`xcodebuild -project Examples/sdk-demo/oms-sdk-demo.xcodeproj -scheme oms-sdk-demo build`) — if applicable +- [ ] Demo app builds (`xcodebuild -project Examples/sdk-demo/oms-sdk-demo.xcodeproj -scheme oms-sdk-demo build CODE_SIGNING_ALLOWED=NO`) ## Related diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7f0b1c7..6f9f794 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,3 +22,6 @@ jobs: - name: Test run: swift test + + - name: Build demo app + run: xcodebuild -project Examples/sdk-demo/oms-sdk-demo.xcodeproj -scheme oms-sdk-demo build CODE_SIGNING_ALLOWED=NO diff --git a/AGENTS.md b/AGENTS.md index dcf22b4..9345aed 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -93,7 +93,7 @@ and target names in shell commands, for example `"Sources/Swift SDK"` and swift build swift test xcodebuild -list -project Examples/sdk-demo/oms-sdk-demo.xcodeproj -xcodebuild -project Examples/sdk-demo/oms-sdk-demo.xcodeproj -scheme oms-sdk-demo build +xcodebuild -project Examples/sdk-demo/oms-sdk-demo.xcodeproj -scheme oms-sdk-demo build CODE_SIGNING_ALLOWED=NO ``` Run `swift test` for SDK changes. For demo app changes, also build the Xcode @@ -127,8 +127,8 @@ execution commands. ## CI/CD CI runs on every PR and push to `master` via `.github/workflows/ci.yml`: -`swift build` and `swift test` are required to pass. Claude GitHub Actions are -defined in `.github/workflows/claude.yml` (mention handler) and +`swift build`, `swift test`, and the `oms-sdk-demo` Xcode build are required to +pass. Claude GitHub Actions are defined in `.github/workflows/claude.yml` (mention handler) and `.github/workflows/claude-code-review.yml` (auto-review on PRs). ## Documentation diff --git a/API.md b/API.md index 0a70541..53f153f 100644 --- a/API.md +++ b/API.md @@ -102,7 +102,7 @@ Most apps create a wallet client through `OMSClient`. Use this initializer only var walletAddress: String ``` -The on-chain address of the active wallet. Empty until a wallet is restored or activated by `completeEmailAuth`, `useWallet`, or `createWallet`. +The read-only on-chain address of the active wallet. Empty until a wallet is restored or activated by `completeEmailAuth`, `useWallet`, or `createWallet`. ### walletId @@ -110,7 +110,7 @@ The on-chain address of the active wallet. Empty until a wallet is restored or a var walletId: String ``` -The server-side wallet ID. Empty until a wallet is restored or activated by `completeEmailAuth`, `useWallet`, or `createWallet`. +The read-only server-side wallet ID. Empty until a wallet is restored or activated by `completeEmailAuth`, `useWallet`, or `createWallet`. ### session diff --git a/Sources/Swift SDK/Clients/WalletClient.swift b/Sources/Swift SDK/Clients/WalletClient.swift index 810b949..e156d93 100644 --- a/Sources/Swift SDK/Clients/WalletClient.swift +++ b/Sources/Swift SDK/Clients/WalletClient.swift @@ -131,7 +131,7 @@ public class WalletClient: @unchecked Sendable { private var _verifier = "" private var _challenge = "" - public var walletAddress: String { + public internal(set) var walletAddress: String { get { withSessionLock { _walletAddress } } @@ -139,7 +139,7 @@ public class WalletClient: @unchecked Sendable { withSessionLock { _walletAddress = newValue } } } - public var walletId: String { + public internal(set) var walletId: String { get { withSessionLock { _walletId } } diff --git a/TESTING.md b/TESTING.md index 29a2077..550c31a 100644 --- a/TESTING.md +++ b/TESTING.md @@ -44,4 +44,4 @@ place them in `Tests/Swift SDKIntegrationTests/` and document prerequisites here | Build without running tests | `swift build` | | Run tests matching a filter | `swift test --filter ` | | Verbose output | `swift test --verbose` | -| Build demo app | `xcodebuild -project Examples/sdk-demo/oms-sdk-demo.xcodeproj -scheme oms-sdk-demo build` | +| Build demo app | `xcodebuild -project Examples/sdk-demo/oms-sdk-demo.xcodeproj -scheme oms-sdk-demo build CODE_SIGNING_ALLOWED=NO` | From 74e4c8320a958c828e3e4694c01040afcfdd268e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andy=20Gr=C3=BCning?= Date: Wed, 10 Jun 2026 13:44:49 +0200 Subject: [PATCH 2/2] updated designs for sdk-demo and trails-actions-demo apps --- .DS_Store | Bin 8196 -> 8196 bytes .github/PULL_REQUEST_TEMPLATE.md | 3 +- .github/workflows/ci.yml | 5 +- AGENTS.md | 12 +- Examples/sdk-demo/Info.plist | 2 + .../oms-sdk-demo.xcodeproj/project.pbxproj | 4 + Examples/sdk-demo/oms-sdk-demo/AppError.swift | 78 +- .../sdk-demo/oms-sdk-demo/ContentView.swift | 691 ++++++++++-------- .../sdk-demo/oms-sdk-demo/OMSSDKDemoApp.swift | 1 + Examples/sdk-demo/styling.gen.swift | 434 +++++++++++ Examples/trails-actions/styling.gen.swift | 678 ++++++----------- .../trails-actions/ContentView.swift | 427 ++++++----- .../trails-actions/DesignSystemBridge.swift | 33 + .../trails-actions/TrailsDemoViewModel.swift | 96 +-- TESTING.md | 3 +- 15 files changed, 1507 insertions(+), 960 deletions(-) create mode 100644 Examples/sdk-demo/styling.gen.swift create mode 100644 Examples/trails-actions/trails-actions/DesignSystemBridge.swift diff --git a/.DS_Store b/.DS_Store index 5f0c85a2cc4db96b3d06d1bd12e2bf15d12968d8..97f033e5980768253efa14be3152c1cfc916de4b 100644 GIT binary patch delta 33 pcmZp1XmOa}&&abeU^hP_&t@KhLgvX6gzs&R=2^qMnO)*9I{>^s3qt?^ delta 45 zcmZp1XmOa}&&azmU^hP_?`9r>LS_MG21kYhhJuvhUIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIUserInterfaceStyle + Light diff --git a/Examples/sdk-demo/oms-sdk-demo.xcodeproj/project.pbxproj b/Examples/sdk-demo/oms-sdk-demo.xcodeproj/project.pbxproj index 345a3e3..18551a0 100644 --- a/Examples/sdk-demo/oms-sdk-demo.xcodeproj/project.pbxproj +++ b/Examples/sdk-demo/oms-sdk-demo.xcodeproj/project.pbxproj @@ -8,9 +8,11 @@ /* Begin PBXBuildFile section */ 3D2708222F98AE7300CD78D9 /* OMS SDK in Frameworks */ = {isa = PBXBuildFile; productRef = 3D2708212F98AE7300CD78D9 /* OMS SDK */; }; + 3D8F2A012F9A000100000001 /* styling.gen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D8F2A002F9A000100000001 /* styling.gen.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 3D8F2A002F9A000100000001 /* styling.gen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = styling.gen.swift; sourceTree = ""; }; 3D65FC962F86D5AC009476BA /* oms-sdk-demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "oms-sdk-demo.app"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -45,6 +47,7 @@ isa = PBXGroup; children = ( 3D65FC982F86D5AC009476BA /* oms-sdk-demo */, + 3D8F2A002F9A000100000001 /* styling.gen.swift */, 3D2708202F98AE7300CD78D9 /* Frameworks */, 3D65FC972F86D5AC009476BA /* Products */, ); @@ -136,6 +139,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3D8F2A012F9A000100000001 /* styling.gen.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Examples/sdk-demo/oms-sdk-demo/AppError.swift b/Examples/sdk-demo/oms-sdk-demo/AppError.swift index b07a123..e731fad 100644 --- a/Examples/sdk-demo/oms-sdk-demo/AppError.swift +++ b/Examples/sdk-demo/oms-sdk-demo/AppError.swift @@ -110,12 +110,80 @@ private func serviceErrorMessage(_ error: WebRPCError, fallback: String) -> Stri extension View { func genericErrorWindow(error: Binding) -> some View { - alert(item: error) { error in - Alert( - title: Text("Something went wrong"), - message: Text(error.message), - dismissButton: .default(Text("OK")) + modifier(GenericErrorDialog(error: error)) + } +} + +private struct GenericErrorDialog: ViewModifier { + @Binding var error: GenericAppError? + + func body(content: Content) -> some View { + content + .overlay { + if let error { + TokenDialog( + title: "Something went wrong", + message: error.message, + primaryTitle: "OK", + primaryAction: { + self.error = nil + } + ) + } + } + } +} + +struct TokenDialog: View { + let title: String + let message: String + let primaryTitle: String + let primaryAction: () -> Void + var secondaryTitle: String? = nil + var secondaryAction: (() -> Void)? = nil + + var body: some View { + ZStack { + DesignTokens.Color.primaryText + .opacity(0.18) + .ignoresSafeArea() + + VStack(alignment: .leading, spacing: 16) { + VStack(alignment: .leading, spacing: 8) { + Text(title) + .font(DesignTokens.Typography.heading) + .foregroundStyle(DesignTokens.Color.primaryText) + .fixedSize(horizontal: false, vertical: true) + + Text(message) + .font(DesignTokens.Typography.caption) + .foregroundStyle(DesignTokens.Color.secondaryText) + .fixedSize(horizontal: false, vertical: true) + } + + HStack(spacing: 12) { + if let secondaryTitle, let secondaryAction { + Button(secondaryTitle, action: secondaryAction) + .buttonStyle(DesignButtonStyle(variant: .secondary)) + } + + Spacer(minLength: 0) + + Button(primaryTitle, action: primaryAction) + .buttonStyle(DesignButtonStyle(variant: .primary)) + } + } + .padding(24) + .frame(maxWidth: 360, alignment: .leading) + .background( + RoundedRectangle(cornerRadius: DesignTokens.Radius.card) + .fill(DesignTokens.Color.surface) + ) + .overlay( + RoundedRectangle(cornerRadius: DesignTokens.Radius.card) + .stroke(DesignTokens.Color.headerBorder, lineWidth: DesignTokens.Stroke.defaultWidth) ) + .padding(24) } } } diff --git a/Examples/sdk-demo/oms-sdk-demo/ContentView.swift b/Examples/sdk-demo/oms-sdk-demo/ContentView.swift index bb7eae4..36e3cc7 100644 --- a/Examples/sdk-demo/oms-sdk-demo/ContentView.swift +++ b/Examples/sdk-demo/oms-sdk-demo/ContentView.swift @@ -97,38 +97,37 @@ fileprivate final class FeeOptionSelectionRequest: Identifiable { // MARK: - Styling private var appBackgroundColor: Color { - #if os(iOS) - Color(.systemGroupedBackground) - #elseif os(macOS) - Color(nsColor: .windowBackgroundColor) - #endif + DesignTokens.Color.page } private var panelBackgroundColor: Color { - #if os(iOS) - Color(.secondarySystemGroupedBackground) - #elseif os(macOS) - Color(nsColor: .textBackgroundColor) - #endif + DesignTokens.Color.surface } private var panelBorderColor: Color { - #if os(iOS) - Color(.separator).opacity(0.45) - #elseif os(macOS) - Color(nsColor: .separatorColor).opacity(0.8) - #endif + DesignTokens.Color.headerBorder } private var fieldBackground: some View { - RoundedRectangle(cornerRadius: 8) + RoundedRectangle(cornerRadius: DesignTokens.Radius.input) .fill(panelBackgroundColor) .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(panelBorderColor, lineWidth: 1) + RoundedRectangle(cornerRadius: DesignTokens.Radius.input) + .stroke(DesignTokens.Color.border, lineWidth: DesignTokens.Stroke.defaultWidth) ) } +private extension View { + func tokenTextInput() -> some View { + self + .textFieldStyle(.plain) + .foregroundStyle(DesignTokens.Color.primaryText) + .tint(DesignTokens.Color.info) + .padding(14) + .background(fieldBackground) + } +} + // MARK: - USDC /// USDC contract on the configured chain. @@ -193,6 +192,7 @@ private func trimBalanceDisplay(_ value: String, maxFractionDigits: Int) -> Stri // MARK: - App State enum AppScreen { + case introduction case login case confirmCode case walletSelection(PendingWalletSelection) @@ -201,7 +201,7 @@ enum AppScreen { @MainActor final class AppViewModel: ObservableObject { - @Published var screen: AppScreen = .login + @Published var screen: AppScreen = .introduction @Published var isLoading: Bool = false @Published var error: GenericAppError? @Published var safariAuthSession: SafariAuthSession? @@ -237,7 +237,7 @@ final class AppViewModel: ObservableObject { func checkSession() async { let hasSession = !oms.wallet.walletAddress.isEmpty - screen = hasSession ? .wallet : .login + screen = hasSession ? .wallet : .introduction } func signOut() { @@ -245,7 +245,7 @@ final class AppViewModel: ObservableObject { try oms.wallet.signOut() safariAuthSession = nil sessionExpiredPrompt = nil - screen = .login + screen = .introduction } catch { present(error) } @@ -471,6 +471,8 @@ struct ContentView: View { var body: some View { Group { switch vm.screen { + case .introduction: + IntroductionWindow() case .login: LoginWindow() case .confirmCode: @@ -509,6 +511,76 @@ struct ContentView: View { } } +// MARK: - Introduction Window + +struct IntroductionWindow: View { + @EnvironmentObject private var vm: AppViewModel + + var body: some View { + NavigationScreenContainer(maxWidth: 440) { + VStack(spacing: 0) { + VStack(alignment: .center, spacing: 18) { + Image("logo") + .resizable() + .scaledToFit() + .frame(width: 72, height: 72) + .accessibilityLabel("Sequence logo") + + VStack(alignment: .center, spacing: 10) { + DesignText("OMS SDK demo", variant: .title) + .multilineTextAlignment(.center) + + DesignText("Try wallet authentication, balances, transactions, contract calls, and message signing from one demo wallet.", variant: .body) + .multilineTextAlignment(.center) + } + } + .padding(.top, 72) + + VStack(alignment: .leading, spacing: 12) { + IntroductionFeatureRow(systemImage: "person.crop.circle.badge.checkmark", title: "Authenticate", subtitle: "Start email or Google wallet auth.") + IntroductionFeatureRow(systemImage: "creditcard", title: "Review assets", subtitle: "Check native and USDC balances by network.") + IntroductionFeatureRow(systemImage: "paperplane", title: "Execute actions", subtitle: "Send value, call contracts, and sign messages.") + } + .padding(.top, 40) + + Spacer() + + Button { + vm.screen = .login + } label: { + label(for: "Get started", loading: false) + } + .buttonStyle(DesignButtonStyle(variant: .primary)) + .padding(.bottom, 24) + } + } + } +} + +private struct IntroductionFeatureRow: View { + let systemImage: String + let title: String + let subtitle: String + + var body: some View { + HStack(alignment: .top, spacing: 14) { + Image(systemName: systemImage) + .font(.system(size: 18, weight: .semibold)) + .foregroundStyle(DesignTokens.Color.info) + .frame(width: 32, height: 32) + .background(DesignTokens.Color.infoSoft) + .clipShape(RoundedRectangle(cornerRadius: DesignTokens.Radius.button)) + + VStack(alignment: .leading, spacing: 3) { + DesignText(title, variant: .caption) + .fontWeight(.bold) + + DesignText(subtitle, variant: .caption) + } + } + } +} + // MARK: - Login Window struct LoginWindow: View { @@ -519,15 +591,14 @@ struct LoginWindow: View { NavigationScreenContainer(maxWidth: 440) { VStack(spacing: 0) { AuthWelcomeHeader( + title: "Sign in to OMS SDK demo", subtitle: "Sign in to continue to your wallet." ) .padding(.top, 64) FieldGroup(title: "Email address", titleStyle: .secondary) { TextField("you@example.com", text: $vm.loginEmail) - .textFieldStyle(.plain) - .padding(14) - .background(fieldBackground) + .tokenTextInput() .autocorrectionDisabled() .focused($emailFocused) #if os(iOS) @@ -540,17 +611,13 @@ struct LoginWindow: View { FieldGroup(title: "Session length", titleStyle: .secondary) { HStack(spacing: 10) { TextField("60", text: $vm.sessionLifetimeText) - .textFieldStyle(.plain) .monospacedDigit() - .padding(14) - .background(fieldBackground) + .tokenTextInput() #if os(iOS) .keyboardType(.numberPad) #endif - Text("seconds") - .font(.subheadline) - .foregroundStyle(.secondary) + DesignText("seconds", variant: .caption) } } .padding(.top, 16) @@ -565,8 +632,7 @@ struct LoginWindow: View { } label: { label(for: "Continue", loading: vm.isLoading) } - .buttonStyle(.borderedProminent) - .controlSize(.large) + .buttonStyle(DesignButtonStyle(variant: .primary)) .disabled(vm.loginEmail.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || vm.sessionLifetimeSeconds == nil || vm.isLoading) Button { @@ -574,8 +640,7 @@ struct LoginWindow: View { } label: { label(for: "Continue with Google", systemImage: "globe", loading: vm.isLoading) } - .buttonStyle(.bordered) - .controlSize(.large) + .buttonStyle(DesignButtonStyle(variant: .secondary)) .disabled(vm.isLoading) .padding(.top, 12) .padding(.bottom, 24) @@ -599,9 +664,7 @@ struct ConfirmCodeWindow: View { ) .padding(.top, 64) - Text("Enter the 6-digit code sent to your email.") - .font(.subheadline) - .foregroundStyle(.secondary) + DesignText("Enter the 6-digit code sent to your email.", variant: .caption) .frame(maxWidth: .infinity, alignment: .leading) .padding(.top, 32) @@ -618,9 +681,18 @@ struct ConfirmCodeWindow: View { } label: { label(for: "Verify", loading: vm.isLoading) } - .buttonStyle(.borderedProminent) - .controlSize(.large) + .buttonStyle(DesignButtonStyle(variant: .primary)) .disabled(codeText.count != 6 || vm.isLoading) + .padding(.bottom, 12) + + Button { + vm.screen = .login + codeText = "" + } label: { + label(for: "Back", loading: false) + } + .buttonStyle(DesignButtonStyle(variant: .secondary)) + .disabled(vm.isLoading) .padding(.bottom, 24) } } @@ -637,6 +709,7 @@ struct WalletSelectionWindow: View { NavigationScreenContainer(maxWidth: 520) { VStack(alignment: .leading, spacing: 0) { AuthWelcomeHeader( + title: "Select a wallet", subtitle: "Select a wallet to finish signing in." ) .padding(.top, 48) @@ -647,6 +720,7 @@ struct WalletSelectionWindow: View { if vm.isLoading { ProgressView() + .tint(DesignTokens.Color.info) .frame(maxWidth: .infinity, alignment: .center) } } @@ -659,8 +733,7 @@ struct WalletSelectionWindow: View { } label: { label(for: "Cancel", systemImage: "xmark", loading: false) } - .buttonStyle(.bordered) - .controlSize(.large) + .buttonStyle(DesignButtonStyle(variant: .secondary)) .disabled(vm.isLoading) .padding(.bottom, 24) } @@ -679,17 +752,18 @@ struct WalletSelectionWindow: View { ) } else { ForEach(pendingSelection.wallets, id: \.id) { wallet in - Button { - Task { await vm.selectWallet(wallet, from: pendingSelection) } - } label: { + Button { + Task { await vm.selectWallet(wallet, from: pendingSelection) } + } label: { WalletSelectionRow( title: shortWalletAddress(wallet.address), subtitle: walletSelectionSubtitle(wallet), leadingText: "0x" ) - } - .buttonStyle(.plain) - .disabled(vm.isLoading) + } + .buttonStyle(.plain) + .foregroundStyle(DesignTokens.Color.primaryText) + .disabled(vm.isLoading) } } } @@ -708,6 +782,7 @@ struct WalletSelectionWindow: View { ) } .buttonStyle(.plain) + .foregroundStyle(DesignTokens.Color.primaryText) .disabled(vm.isLoading) } } @@ -743,11 +818,7 @@ private struct ManualWalletSelectionToggle: View { @EnvironmentObject private var vm: AppViewModel var body: some View { - Toggle(isOn: $vm.useManualWalletSelection) { - Text("Use manual wallet selection") - .font(.subheadline) - .fontWeight(.medium) - } + DesignToggle("Use manual wallet selection", isOn: $vm.useManualWalletSelection) .toggleStyle(.switch) .frame(maxWidth: .infinity, alignment: .leading) } @@ -763,27 +834,24 @@ private struct WalletSelectionRow: View { HStack(spacing: 12) { Text(leadingText) .font(.system(size: 14, weight: .bold)) - .foregroundStyle(.primary) + .foregroundStyle(DesignTokens.Color.primaryText) .frame(width: 38, height: 38) .background( - RoundedRectangle(cornerRadius: 8) - .fill(appBackgroundColor) + RoundedRectangle(cornerRadius: DesignTokens.Radius.button) + .fill(DesignTokens.Color.secondarySurface) ) .overlay( - RoundedRectangle(cornerRadius: 8) + RoundedRectangle(cornerRadius: DesignTokens.Radius.button) .stroke(panelBorderColor, lineWidth: 1) ) VStack(alignment: .leading, spacing: 3) { - Text(title) - .font(.subheadline.weight(.semibold)) - .foregroundStyle(.primary) + DesignText(title, variant: .caption) + .fontWeight(.semibold) .lineLimit(1) .truncationMode(.middle) - Text(subtitle) - .font(.caption) - .foregroundStyle(.secondary) + DesignText(subtitle, variant: .caption) .lineLimit(2) .truncationMode(.middle) } @@ -793,21 +861,21 @@ private struct WalletSelectionRow: View { if isEnabled { Image(systemName: "chevron.right") .font(.caption.weight(.semibold)) - .foregroundStyle(.secondary) + .foregroundStyle(DesignTokens.Color.secondaryText) } } .padding(14) .frame(maxWidth: .infinity, minHeight: 64, alignment: .leading) .background( - RoundedRectangle(cornerRadius: 8) + RoundedRectangle(cornerRadius: DesignTokens.Radius.input) .fill(panelBackgroundColor) ) .overlay( - RoundedRectangle(cornerRadius: 8) + RoundedRectangle(cornerRadius: DesignTokens.Radius.input) .stroke(panelBorderColor, lineWidth: 1) ) .opacity(isEnabled ? 1 : 0.72) - .contentShape(RoundedRectangle(cornerRadius: 8)) + .contentShape(RoundedRectangle(cornerRadius: DesignTokens.Radius.input)) } } @@ -815,9 +883,6 @@ private struct WalletSelectionRow: View { struct WalletWindow: View { @EnvironmentObject private var vm: AppViewModel - @State private var showSendWindow: Bool = false - @State private var showCallContractWindow: Bool = false - @State private var showSignMessageWindow: Bool = false @State private var didCopy: Bool = false @State private var nativeBalance: String = "—" @State private var nativeBalanceRaw: String = "" @@ -872,46 +937,79 @@ struct WalletWindow: View { } var body: some View { + TabView { + walletTab + .tabItem { + Label("Wallet", systemImage: "wallet.pass") + } + + SendTransactionWindow(showsCloseButton: false) + .environmentObject(vm) + .tabItem { + Label("Send", systemImage: "arrow.up.circle") + } + + CallContractWindow(onCompleted: { + Task { await refreshBalance() } + }, showsCloseButton: false) + .environmentObject(vm) + .tabItem { + Label("Call", systemImage: "curlybraces") + } + + SignMessageWindow(showsCloseButton: false) + .environmentObject(vm) + .tabItem { + Label("Sign", systemImage: "signature") + } + } + .tint(DesignTokens.Color.info) + #if os(macOS) + .frame(minWidth: 640, minHeight: 560) + #endif + } + + private var walletTab: some View { NavigationStack { ScrollView { VStack(spacing: 24) { + walletHeader walletAddressBar - walletActions assetSection } .frame(maxWidth: 560) - .padding(.top, 8) + .padding(.top, 24) .padding(.horizontal, 20) - .padding(.bottom, 24) + .padding(.bottom, 88) .frame(maxWidth: .infinity) } .background(appBackgroundColor.ignoresSafeArea()) - .navigationTitle("My Wallet") - .appNavigationTitleDisplayMode(.large) .task { await refreshBalance() } .onChange(of: selectedNetwork) { Task { await refreshBalance() } } - .sheet(isPresented: $showSendWindow) { - SendTransactionWindow() - .environmentObject(vm) - } - .sheet(isPresented: $showCallContractWindow) { - CallContractWindow(onCompleted: { - Task { await refreshBalance() } - }) - .environmentObject(vm) - } - .sheet(isPresented: $showSignMessageWindow) { - SignMessageWindow() - .environmentObject(vm) + } + } + + private var walletHeader: some View { + HStack(alignment: .center, spacing: 12) { + Text("Wallet") + .font(.custom(DesignTokens.Typography.family, size: 34).weight(.bold)) + .foregroundStyle(DesignTokens.Color.primaryText) + + Spacer() + + DesignIconButton( + systemImage: "rectangle.portrait.and.arrow.right", + accessibilityLabel: "Sign out" + ) { + vm.signOut() } + .help("Sign out") } - #if os(macOS) - .frame(minWidth: 640, minHeight: 560) - #endif + .frame(maxWidth: .infinity, alignment: .leading) } private var walletAddressBar: some View { @@ -919,6 +1017,7 @@ struct WalletWindow: View { HStack(spacing: 6) { Text(collapsedAddress(vm.oms.wallet.walletAddress)) .font(.system(size: 22, weight: .semibold, design: .monospaced)) + .foregroundStyle(DesignTokens.Color.primaryText) .lineLimit(1) .truncationMode(.middle) .textSelection(.enabled) @@ -933,16 +1032,14 @@ struct WalletWindow: View { } label: { Image(systemName: didCopy ? "checkmark" : "doc.on.doc") .font(.system(size: 18)) - .foregroundStyle(didCopy ? Color.green : Color.accentColor) + .foregroundStyle(didCopy ? DesignTokens.Color.success : DesignTokens.Color.info) } .buttonStyle(.plain) .disabled(vm.oms.wallet.walletAddress.isEmpty) - .help(didCopy ? "Copied!" : "Copy address") + .help(didCopy ? "Copied" : "Copy address") } - Text(sessionEmail) - .font(.subheadline) - .foregroundStyle(.secondary) + DesignText(sessionEmail, variant: .caption) .lineLimit(1) .truncationMode(.middle) .textSelection(.enabled) @@ -953,58 +1050,34 @@ struct WalletWindow: View { .padding(.bottom, 8) } - private var walletActions: some View { - HStack(spacing: 0) { - walletActionButton("Send", systemImage: "arrow.up.circle") { - showSendWindow = true - } - Divider().frame(height: 40) - walletActionButton("Contract", systemImage: "arrow.up.circle") { - showCallContractWindow = true - } - Divider().frame(height: 40) - walletActionButton("Sign", systemImage: "signature") { - showSignMessageWindow = true - } - Divider().frame(height: 40) - walletActionButton("Sign Out", systemImage: "rectangle.portrait.and.arrow.right") { - vm.signOut() - } - } - .background( - RoundedRectangle(cornerRadius: 8) - .fill(panelBackgroundColor) - ) - .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(panelBorderColor, lineWidth: 1) - ) - } - private var assetSection: some View { VStack(spacing: 12) { HStack(spacing: 12) { Text("Assets") .font(.headline) + .foregroundStyle(DesignTokens.Color.primaryText) Spacer() - Picker("", selection: $selectedNetwork) { - ForEach(supportedNetworks, id: \.self) { network in - Text(network.displayName).tag(network) - } + Picker("", selection: $selectedNetwork) { + ForEach(supportedNetworks, id: \.self) { network in + Text(network.displayName).tag(network) } - .pickerStyle(.menu) - .labelsHidden() - .fixedSize(horizontal: true, vertical: false) + } + .pickerStyle(.menu) + .tint(DesignTokens.Color.primaryText) + .foregroundStyle(DesignTokens.Color.primaryText) + .labelsHidden() + .fixedSize(horizontal: true, vertical: false) - Button { - Task { await refreshBalance() } - } label: { - Image(systemName: "arrow.clockwise") - } - .buttonStyle(.plain) - .disabled(isFetchingBalance) + Button { + Task { await refreshBalance() } + } label: { + Image(systemName: "arrow.clockwise") + .foregroundStyle(DesignTokens.Color.info) + } + .buttonStyle(.plain) + .disabled(isFetchingBalance) .help("Refresh balance") } @@ -1014,25 +1087,27 @@ struct WalletWindow: View { } private var nativeTokenCard: some View { - HStack(spacing: 14) { + DesignCard { + HStack(spacing: 14) { ZStack { Circle() - .fill(Color(red: 0.28, green: 0.54, blue: 0.34)) + .fill(DesignTokens.Color.success) .frame(width: 44, height: 44) Image(systemName: "hexagon.fill") .font(.system(size: 21, weight: .semibold)) - .foregroundStyle(.white) + .foregroundStyle(DesignTokens.Color.primaryButtonText) } VStack(alignment: .leading, spacing: 2) { Text("\(selectedNetwork.displayName) Native") .font(.subheadline.weight(.semibold)) + .foregroundStyle(DesignTokens.Color.primaryText) .lineLimit(1) .minimumScaleFactor(0.8) Text(nativeTokenSymbol(for: selectedNetwork)) .font(.caption) - .foregroundStyle(.secondary) + .foregroundStyle(DesignTokens.Color.secondaryText) } Spacer() @@ -1041,46 +1116,42 @@ struct WalletWindow: View { if isFetchingBalance { ProgressView() .controlSize(.small) + .tint(DesignTokens.Color.info) } else { Text(nativeBalance) .font(.subheadline.weight(.semibold)) + .foregroundStyle(DesignTokens.Color.primaryText) .monospacedDigit() Text(nativeTokenSymbol(for: selectedNetwork)) .font(.caption) - .foregroundStyle(.secondary) + .foregroundStyle(DesignTokens.Color.secondaryText) .monospacedDigit() } } } - .padding(16) - .background( - RoundedRectangle(cornerRadius: 8) - .fill(panelBackgroundColor) - ) - .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(panelBorderColor, lineWidth: 1) - ) + } } private var usdcCard: some View { - HStack(spacing: 14) { + DesignCard { + HStack(spacing: 14) { ZStack { Circle() - .fill(Color(red: 0.16, green: 0.45, blue: 0.90)) + .fill(DesignTokens.Color.info) .frame(width: 44, height: 44) Text("$") .font(.system(size: 22, weight: .bold)) - .foregroundStyle(.white) + .foregroundStyle(DesignTokens.Color.primaryButtonText) } VStack(alignment: .leading, spacing: 2) { Text("USD Coin") .font(.subheadline.weight(.semibold)) + .foregroundStyle(DesignTokens.Color.primaryText) Text("USDC") .font(.caption) - .foregroundStyle(.secondary) + .foregroundStyle(DesignTokens.Color.secondaryText) } Spacer() @@ -1089,26 +1160,20 @@ struct WalletWindow: View { if isFetchingBalance { ProgressView() .controlSize(.small) + .tint(DesignTokens.Color.info) } else { Text(usdcBalance) .font(.subheadline.weight(.semibold)) + .foregroundStyle(DesignTokens.Color.primaryText) .monospacedDigit() Text("$\(usdcBalance)") .font(.caption) - .foregroundStyle(.secondary) + .foregroundStyle(DesignTokens.Color.secondaryText) .monospacedDigit() } } } - .padding(16) - .background( - RoundedRectangle(cornerRadius: 8) - .fill(panelBackgroundColor) - ) - .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(panelBorderColor, lineWidth: 1) - ) + } } private func collapsedAddress(_ address: String) -> String { @@ -1121,26 +1186,6 @@ struct WalletWindow: View { vm.oms.wallet.session.sessionEmail ?? "Email unavailable" } - private func walletActionButton( - _ title: String, - systemImage: String, - action: @escaping () -> Void - ) -> some View { - Button(action: action) { - VStack(spacing: 6) { - Image(systemName: systemImage) - .font(.system(size: 20)) - Text(title) - .font(.caption.weight(.medium)) - .lineLimit(1) - .minimumScaleFactor(0.8) - } - .foregroundStyle(Color.accentColor) - .frame(maxWidth: .infinity) - .padding(.vertical, 14) - } - .buttonStyle(.plain) - } } // MARK: - Sign Message Window @@ -1148,6 +1193,7 @@ struct WalletWindow: View { struct SignMessageWindow: View { @EnvironmentObject private var vm: AppViewModel @Environment(\.dismiss) private var dismiss + var showsCloseButton: Bool = true @State private var messageText: String = "" @State private var network: Network = Network.polygonAmoy @@ -1160,7 +1206,8 @@ struct SignMessageWindow: View { title: "Sign message", subtitle: "Create a wallet signature for the selected network.", minWidth: 440, - minHeight: 380 + minHeight: 380, + showsCloseButton: showsCloseButton ) { FieldGroup(title: "Network") { Picker("Network", selection: $network) { @@ -1169,12 +1216,14 @@ struct SignMessageWindow: View { } } .pickerStyle(.menu) + .tint(DesignTokens.Color.primaryText) + .foregroundStyle(DesignTokens.Color.primaryText) .frame(maxWidth: .infinity, alignment: .leading) } FieldGroup(title: "Message") { TextField("Enter message", text: $messageText) - .textFieldStyle(.roundedBorder) + .tokenTextInput() } Button { @@ -1195,8 +1244,7 @@ struct SignMessageWindow: View { } label: { label(for: "Sign message", systemImage: "signature", loading: isSigning) } - .buttonStyle(.borderedProminent) - .controlSize(.large) + .buttonStyle(DesignButtonStyle(variant: .primary)) .disabled(messageText.isEmpty || isSigning) if !signature.isEmpty { @@ -1212,11 +1260,12 @@ struct SignMessageWindow: View { struct SendTransactionWindow: View { @EnvironmentObject private var vm: AppViewModel @Environment(\.dismiss) private var dismiss + var showsCloseButton: Bool = true @State private var toText: String = "" @State private var amountText: String = "1000" @State private var network: Network = Network.polygonAmoy - @State private var result: String = "" + @State private var result: SendTransactionResponse? @State private var isSending: Bool = false @State private var error: GenericAppError? @@ -1225,7 +1274,8 @@ struct SendTransactionWindow: View { title: "Send transaction", subtitle: "Transfer native token value from this wallet.", minWidth: 460, - minHeight: 460 + minHeight: 460, + showsCloseButton: showsCloseButton ) { FieldGroup(title: "Network") { Picker("Network", selection: $network) { @@ -1234,12 +1284,14 @@ struct SendTransactionWindow: View { } } .pickerStyle(.menu) + .tint(DesignTokens.Color.primaryText) + .foregroundStyle(DesignTokens.Color.primaryText) .frame(maxWidth: .infinity, alignment: .leading) } FieldGroup(title: "To address") { TextField("0x...", text: $toText) - .textFieldStyle(.roundedBorder) + .tokenTextInput() .autocorrectionDisabled() #if os(iOS) .textInputAutocapitalization(.never) @@ -1248,7 +1300,7 @@ struct SendTransactionWindow: View { FieldGroup(title: "Amount") { TextField("Enter amount", text: $amountText) - .textFieldStyle(.roundedBorder) + .tokenTextInput() #if os(iOS) .keyboardType(.decimalPad) #endif @@ -1268,7 +1320,7 @@ struct SendTransactionWindow: View { try await vm.selectFeeOption(options) } ) - result = formatTransactionResult(txResult) + result = txResult } catch { self.error = GenericAppError(error) } @@ -1276,12 +1328,11 @@ struct SendTransactionWindow: View { } label: { label(for: "Execute transaction", systemImage: "paperplane", loading: isSending) } - .buttonStyle(.borderedProminent) - .controlSize(.large) + .buttonStyle(DesignButtonStyle(variant: .primary)) .disabled(amountText.isEmpty || toText.isEmpty || isSending) - if !result.isEmpty { - ResultPanel(title: "Transaction result", text: result) + if let result { + TransactionResultPanel(result: result) } } .genericErrorWindow(error: $error) @@ -1341,6 +1392,7 @@ struct CallContractWindow: View { /// Invoked after a successful contract call so the parent view can refresh /// any derived state (e.g. balances). var onCompleted: (() -> Void)? = nil + var showsCloseButton: Bool = true @State private var contractText: String = "0x41e94eb019c0762f9bfcf9fb1e58725bfb0e7582" @State private var methodText: String = "transfer" @@ -1349,7 +1401,7 @@ struct CallContractWindow: View { AbiArgInput(type: "address", value: "0xE5E8B483FfC05967FcFed58cc98D053265af6D99"), AbiArgInput(type: "uint256", value: "1000000"), ] - @State private var result: String = "" + @State private var result: SendTransactionResponse? @State private var isSending: Bool = false @State private var error: GenericAppError? @@ -1358,7 +1410,8 @@ struct CallContractWindow: View { title: "Call contract", subtitle: "Build a contract method call with ABI arguments.", minWidth: 500, - minHeight: 560 + minHeight: 560, + showsCloseButton: showsCloseButton ) { FieldGroup(title: "Network") { Picker("Network", selection: $network) { @@ -1367,12 +1420,14 @@ struct CallContractWindow: View { } } .pickerStyle(.menu) + .tint(DesignTokens.Color.primaryText) + .foregroundStyle(DesignTokens.Color.primaryText) .frame(maxWidth: .infinity, alignment: .leading) } FieldGroup(title: "Contract") { TextField("0x...", text: $contractText) - .textFieldStyle(.roundedBorder) + .tokenTextInput() .autocorrectionDisabled() #if os(iOS) .textInputAutocapitalization(.never) @@ -1381,7 +1436,7 @@ struct CallContractWindow: View { FieldGroup(title: "Method") { TextField("e.g. transfer", text: $methodText) - .textFieldStyle(.roundedBorder) + .tokenTextInput() .autocorrectionDisabled() #if os(iOS) .textInputAutocapitalization(.never) @@ -1395,6 +1450,7 @@ struct CallContractWindow: View { args.append(AbiArgInput()) } label: { Image(systemName: "plus.circle.fill") + .foregroundStyle(DesignTokens.Color.info) } .buttonStyle(.plain) .help("Add argument") @@ -1437,7 +1493,7 @@ struct CallContractWindow: View { try await vm.selectFeeOption(options) } ) - result = formatTransactionResult(txResult) + result = txResult onCompleted?() } catch { self.error = GenericAppError(error) @@ -1446,12 +1502,11 @@ struct CallContractWindow: View { } label: { label(for: "Execute transaction", systemImage: "paperplane", loading: isSending) } - .buttonStyle(.borderedProminent) - .controlSize(.large) + .buttonStyle(DesignButtonStyle(variant: .primary)) .disabled(contractText.isEmpty || methodText.isEmpty || isSending) - if !result.isEmpty { - ResultPanel(title: "Transaction result", text: result) + if let result { + TransactionResultPanel(result: result) } } .genericErrorWindow(error: $error) @@ -1463,7 +1518,7 @@ struct CallContractWindow: View { private func abiTypeField(arg: Binding) -> some View { TextField("type (e.g. uint256)", text: arg.type) - .textFieldStyle(.roundedBorder) + .tokenTextInput() .autocorrectionDisabled() #if os(iOS) .textInputAutocapitalization(.never) @@ -1472,7 +1527,7 @@ struct CallContractWindow: View { private func abiValueField(arg: Binding) -> some View { TextField("value", text: arg.value) - .textFieldStyle(.roundedBorder) + .tokenTextInput() .autocorrectionDisabled() #if os(iOS) .textInputAutocapitalization(.never) @@ -1484,7 +1539,7 @@ struct CallContractWindow: View { args.removeAll { $0.id == arg.id } } label: { Image(systemName: "minus.circle.fill") - .foregroundStyle(.secondary) + .foregroundStyle(DesignTokens.Color.secondaryText) } .buttonStyle(.plain) .disabled(args.count <= 1) @@ -1538,6 +1593,7 @@ private struct FeeOptionSelectionWindow: View { Button("Cancel", role: .cancel) { cancel() } + .buttonStyle(DesignButtonStyle(variant: .secondary)) Spacer() @@ -1546,6 +1602,7 @@ private struct FeeOptionSelectionWindow: View { select(request.options[firstAvailableIndex]) } } + .buttonStyle(DesignButtonStyle(variant: .secondary)) .disabled(firstAvailableIndex == nil) Button("Select fee") { @@ -1553,8 +1610,8 @@ private struct FeeOptionSelectionWindow: View { select(selectedOption) } } - .buttonStyle(.borderedProminent) - .disabled(selectedOption == nil) + .buttonStyle(DesignButtonStyle(variant: .primary)) + .disabled(selectedOption == nil || selectedOption.map(hasEnoughBalance) == false) } } .onAppear { @@ -1589,36 +1646,28 @@ private struct FeeOptionRow: View { HStack(alignment: .top, spacing: 12) { Image(systemName: isSelected ? "checkmark.circle.fill" : "circle") .font(.title3) - .foregroundStyle(isSelected ? Color.accentColor : Color.secondary) + .foregroundStyle(isSelected ? DesignTokens.Color.info : DesignTokens.Color.secondaryText) .frame(width: 24, height: 24) VStack(alignment: .leading, spacing: 8) { HStack(alignment: .firstTextBaseline, spacing: 8) { Text(feeTokenLabel(option)) .font(.headline) + .foregroundStyle(DesignTokens.Color.primaryText) .lineLimit(1) Text(feeAmountLabel(option)) .font(.subheadline) - .foregroundStyle(.secondary) + .foregroundStyle(DesignTokens.Color.secondaryText) .lineLimit(1) } - Text(balanceStatusLabel(option)) - .font(.caption) - .foregroundStyle(balanceStatusColor(option)) + DesignBadge(balanceStatusLabel(option), variant: balanceStatusBadgeVariant(option)) .lineLimit(1) - VStack(alignment: .leading, spacing: 3) { - Text("Available: \(option.available ?? "unknown")") - Text("Raw balance: \(option.availableRaw ?? "unknown")") - Text("Raw fee: \(option.feeOption.value)") - if let decimals = option.decimals { - Text("Decimals: \(decimals)") - } - } - .font(.caption.monospaced()) - .foregroundStyle(.secondary) + Text("Available \(option.available ?? "unknown")") + .font(.caption.monospacedDigit()) + .foregroundStyle(DesignTokens.Color.secondaryText) .textSelection(.enabled) } .frame(maxWidth: .infinity, alignment: .leading) @@ -1626,18 +1675,16 @@ private struct FeeOptionRow: View { Button("Select") { onConfirm() } - .buttonStyle(.bordered) + .buttonStyle(DesignButtonStyle(variant: .secondary)) } .padding(14) .frame(maxWidth: .infinity, alignment: .leading) - .background( - RoundedRectangle(cornerRadius: 8) - .fill(panelBackgroundColor) - ) + .background(isSelected ? DesignTokens.Color.infoSoft : panelBackgroundColor) .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(isSelected ? Color.accentColor : panelBorderColor, lineWidth: isSelected ? 2 : 1) + RoundedRectangle(cornerRadius: DesignTokens.Radius.card) + .stroke(isSelected ? DesignTokens.Color.focusRing : panelBorderColor, lineWidth: isSelected ? 2 : 1) ) + .clipShape(RoundedRectangle(cornerRadius: DesignTokens.Radius.card)) .contentShape(Rectangle()) .onTapGesture { onSelect() @@ -1672,11 +1719,11 @@ private func balanceStatusLabel(_ option: FeeOptionWithBalance) -> String { return comparison == .orderedAscending ? "Insufficient balance" : "Enough balance" } -private func balanceStatusColor(_ option: FeeOptionWithBalance) -> Color { +private func balanceStatusBadgeVariant(_ option: FeeOptionWithBalance) -> DesignBadgeVariant { guard let comparison = compareBalanceToFee(option) else { - return .secondary + return .neutral } - return comparison == .orderedAscending ? .red : .green + return comparison == .orderedAscending ? .danger : .success } private func hasEnoughBalance(_ option: FeeOptionWithBalance) -> Bool { @@ -1711,14 +1758,6 @@ private func normalizedUnsignedInteger(_ value: String?) -> String? { return stripped.isEmpty ? "0" : String(stripped) } -private func formatTransactionResult(_ result: SendTransactionResponse) -> String { - """ - txnId: \(result.txnId) - status: \(result.status.wireValue) - txnHash: \(result.txnHash ?? "nil") - """ -} - // MARK: - Helpers #if os(iOS) @@ -1755,6 +1794,7 @@ private extension View { } private struct AuthWelcomeHeader: View { + var title: String = "Welcome" let subtitle: String var body: some View { @@ -1766,14 +1806,10 @@ private struct AuthWelcomeHeader: View { .accessibilityLabel("Sequence logo") VStack(alignment: .center, spacing: 6) { - Text("Welcome") - .font(.largeTitle) - .fontWeight(.bold) + DesignText(title, variant: .title) .multilineTextAlignment(.center) - Text(subtitle) - .font(.subheadline) - .foregroundStyle(.secondary) + DesignText(subtitle, variant: .caption) .multilineTextAlignment(.center) } } @@ -1798,10 +1834,11 @@ private struct NavigationScreenContainer: View { VStack(spacing: 0) { content } - .frame(maxWidth: maxWidth, maxHeight: .infinity) - .padding(.horizontal, 24) - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(appBackgroundColor.ignoresSafeArea()) + .frame(maxWidth: maxWidth, maxHeight: .infinity) + .padding(.horizontal, 24) + .foregroundStyle(DesignTokens.Color.primaryText) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(appBackgroundColor.ignoresSafeArea()) } #if os(macOS) .frame(minWidth: maxWidth + 80, minHeight: 520) @@ -1816,6 +1853,7 @@ private struct ModalContainer: View { let subtitle: String? let minWidth: CGFloat let minHeight: CGFloat + let showsCloseButton: Bool let content: Content init( @@ -1823,12 +1861,14 @@ private struct ModalContainer: View { subtitle: String? = nil, minWidth: CGFloat, minHeight: CGFloat, + showsCloseButton: Bool = true, @ViewBuilder content: () -> Content ) { self.title = title self.subtitle = subtitle self.minWidth = minWidth self.minHeight = minHeight + self.showsCloseButton = showsCloseButton self.content = content() } @@ -1836,10 +1876,25 @@ private struct ModalContainer: View { NavigationStack { ScrollView { VStack(alignment: .leading, spacing: 20) { + HStack(alignment: .center, spacing: 12) { + DesignText(title, variant: .title) + .fixedSize(horizontal: false, vertical: true) + + Spacer() + + if showsCloseButton { + DesignIconButton( + systemImage: "xmark", + accessibilityLabel: "Close" + ) { + dismiss() + } + .help("Close") + } + } + if let subtitle { - Text(subtitle) - .font(.subheadline) - .foregroundStyle(.secondary) + DesignText(subtitle, variant: .caption) .frame(maxWidth: .infinity, alignment: .leading) } @@ -1848,18 +1903,10 @@ private struct ModalContainer: View { .frame(maxWidth: 560, alignment: .leading) .padding(.horizontal, 20) .padding(.vertical, 24) + .foregroundStyle(DesignTokens.Color.primaryText) .frame(maxWidth: .infinity) } .background(appBackgroundColor.ignoresSafeArea()) - .navigationTitle(title) - .appNavigationTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button("Close") { - dismiss() - } - } - } } #if os(macOS) .frame(minWidth: minWidth, minHeight: minHeight) @@ -1887,10 +1934,8 @@ private struct FieldGroup: View { var body: some View { VStack(alignment: .leading, spacing: 8) { - Text(title) - .font(.subheadline) + DesignText(title, variant: titleStyle == .secondary ? .caption : .body) .fontWeight(.semibold) - .foregroundStyle(titleStyle == .secondary ? Color.secondary : Color.primary) content } @@ -1916,6 +1961,8 @@ private struct VerificationCodeInput: View { TextField("", text: codeBinding) .autocorrectionDisabled() .focused($isFocused) + .foregroundStyle(DesignTokens.Color.primaryText) + .tint(DesignTokens.Color.info) .frame(width: 1, height: 1) .opacity(0.01) .accessibilityLabel("6-digit code") @@ -1954,14 +2001,15 @@ private struct VerificationCodeInput: View { return Text(digit) .font(.title3.monospacedDigit().weight(.semibold)) + .foregroundStyle(DesignTokens.Color.primaryText) .frame(width: size, height: size) .background( - RoundedRectangle(cornerRadius: 8) + RoundedRectangle(cornerRadius: DesignTokens.Radius.input) .fill(panelBackgroundColor) ) .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(isActive ? Color.accentColor : panelBorderColor, lineWidth: isActive ? 2 : 1) + RoundedRectangle(cornerRadius: DesignTokens.Radius.input) + .stroke(isActive ? DesignTokens.Color.focusRing : panelBorderColor, lineWidth: isActive ? 2 : 1) ) } @@ -1990,19 +2038,11 @@ private struct Panel: View { } var body: some View { - VStack(alignment: .leading, spacing: 12) { + DesignCard { + VStack(alignment: .leading, spacing: 12) { content } - .padding(16) - .frame(maxWidth: .infinity, alignment: .leading) - .background( - RoundedRectangle(cornerRadius: 8) - .fill(panelBackgroundColor) - ) - .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(panelBorderColor, lineWidth: 1) - ) + } } } @@ -2012,8 +2052,7 @@ private struct ResultPanel: View { var body: some View { Panel { - Text(title) - .font(.subheadline) + DesignText(title, variant: .body) .fontWeight(.semibold) CopyableResult(text: text) @@ -2021,6 +2060,38 @@ private struct ResultPanel: View { } } +private struct TransactionResultPanel: View { + let result: SendTransactionResponse + + var body: some View { + Panel { + DesignText("Transaction result", variant: .body) + .fontWeight(.semibold) + + ResultRow(label: "Status", value: result.status.wireValue) + ResultRow(label: "Transaction ID", value: result.txnId) + + if let txnHash = result.txnHash, !txnHash.isEmpty { + ResultRow(label: "Transaction hash", value: txnHash) + } + } + } +} + +private struct ResultRow: View { + let label: String + let value: String + + var body: some View { + VStack(alignment: .leading, spacing: 6) { + DesignText(label, variant: .caption) + .fontWeight(.semibold) + + CopyableResult(text: value) + } + } +} + /// A truncated, monospaced result string with a copy-to-clipboard button. struct CopyableResult: View { let text: String @@ -2031,7 +2102,7 @@ struct CopyableResult: View { Text(text) .font(.footnote) .monospaced() - .foregroundStyle(.secondary) + .foregroundStyle(DesignTokens.Color.secondaryText) .lineLimit(1) .truncationMode(.middle) .textSelection(.enabled) @@ -2046,9 +2117,10 @@ struct CopyableResult: View { } } label: { Image(systemName: didCopy ? "checkmark" : "doc.on.doc") + .foregroundStyle(didCopy ? DesignTokens.Color.success : DesignTokens.Color.info) } - .buttonStyle(.bordered) - .help(didCopy ? "Copied!" : "Copy") + .buttonStyle(DesignButtonStyle(variant: .secondary)) + .help(didCopy ? "Copied" : "Copy") } } } @@ -2060,6 +2132,7 @@ private func label(for title: String, systemImage: String? = nil, loading: Bool) if loading { ProgressView() .progressViewStyle(.circular) + .tint(DesignTokens.Color.info) } else if let systemImage { Label(title, systemImage: systemImage) .frame(maxWidth: .infinity) @@ -2077,19 +2150,36 @@ private extension View { retry: @escaping (SessionExpiredPrompt) -> Void, dismiss: @escaping () -> Void ) -> some View { - alert(item: prompt) { prompt in - Alert( - title: Text("Session expired"), - message: Text(sessionExpiredMessage(prompt)), - primaryButton: .default(Text("Accept")) { - retry(prompt) - }, - secondaryButton: .cancel(Text("Not now"), action: dismiss) - ) + overlay { + if let prompt = prompt.wrappedValue { + TokenDialog( + title: "Session expired", + message: sessionExpiredMessage(prompt), + primaryTitle: "Sign in again", + primaryAction: { + retry(prompt) + }, + secondaryTitle: "Not now", + secondaryAction: dismiss + ) + } } } } +private extension View { + func tokenPlainIconButtonFrame() -> some View { + self + .frame(width: 40, height: 40) + .background(DesignTokens.Color.surface) + .clipShape(RoundedRectangle(cornerRadius: DesignTokens.Radius.button)) + .overlay( + RoundedRectangle(cornerRadius: DesignTokens.Radius.button) + .stroke(DesignTokens.Color.border, lineWidth: DesignTokens.Stroke.defaultWidth) + ) + } +} + private func sessionExpiredMessage(_ prompt: SessionExpiredPrompt) -> String { if let email = prompt.email { return "Your wallet session expired. Do you want to sign in with \(email) again?" @@ -2104,6 +2194,11 @@ private func sessionExpiredMessage(_ prompt: SessionExpiredPrompt) -> String { .environmentObject(AppViewModel()) } +#Preview("Introduction") { + IntroductionWindow() + .environmentObject(AppViewModel()) +} + #Preview("Confirm Code") { ConfirmCodeWindow() .environmentObject(AppViewModel()) diff --git a/Examples/sdk-demo/oms-sdk-demo/OMSSDKDemoApp.swift b/Examples/sdk-demo/oms-sdk-demo/OMSSDKDemoApp.swift index 5b9478c..c3c7a97 100644 --- a/Examples/sdk-demo/oms-sdk-demo/OMSSDKDemoApp.swift +++ b/Examples/sdk-demo/oms-sdk-demo/OMSSDKDemoApp.swift @@ -5,6 +5,7 @@ struct OMSSDKDemoApp: App { var body: some Scene { WindowGroup { ContentView() + .preferredColorScheme(.light) } } } diff --git a/Examples/sdk-demo/styling.gen.swift b/Examples/sdk-demo/styling.gen.swift new file mode 100644 index 0000000..dddc59d --- /dev/null +++ b/Examples/sdk-demo/styling.gen.swift @@ -0,0 +1,434 @@ +// Generated by design-architect. Do not edit manually. +// Theme: OMS (oms) +// swiftlint:disable all + +import SwiftUI + +private extension Color { + init(designHex hex: String) { + let value = hex.trimmingCharacters(in: CharacterSet(charactersIn: "#")) + let scanner = Scanner(string: value) + var rgba: UInt64 = 0 + scanner.scanHexInt64(&rgba) + + let red: Double + let green: Double + let blue: Double + let alpha: Double + + if value.count == 8 { + red = Double((rgba >> 24) & 0xff) / 255 + green = Double((rgba >> 16) & 0xff) / 255 + blue = Double((rgba >> 8) & 0xff) / 255 + alpha = Double(rgba & 0xff) / 255 + } else { + red = Double((rgba >> 16) & 0xff) / 255 + green = Double((rgba >> 8) & 0xff) / 255 + blue = Double(rgba & 0xff) / 255 + alpha = 1 + } + + self.init(.sRGB, red: red, green: green, blue: blue, opacity: alpha) + } +} + +public enum DesignTheme { + public static let id = "oms" + public static let name = "OMS" + public static let description = "Light financial product theme for OMS dashboards." +} + +public enum DesignTokens { + public enum Color { + public static let page = SwiftUI.Color(designHex: "#F6F6FC") + public static let surface = SwiftUI.Color(designHex: "#FFFFFF") + public static let secondarySurface = SwiftUI.Color(designHex: "#F6F6FC") + public static let header = SwiftUI.Color(designHex: "#FFFFFF") + public static let headerBorder = SwiftUI.Color(designHex: "#DFE3F0") + public static let footer = SwiftUI.Color(designHex: "#FFFFFF") + public static let primaryText = SwiftUI.Color(designHex: "#141635") + public static let secondaryText = SwiftUI.Color(designHex: "#64708F") + public static let placeholderText = SwiftUI.Color(designHex: "#929EBA") + public static let border = SwiftUI.Color(designHex: "#C8CFE1") + public static let focusRing = SwiftUI.Color(designHex: "#B78EEF") + public static let brand = SwiftUI.Color(designHex: "#670DE5") + public static let hover = SwiftUI.Color(designHex: "#F6F6FC") + public static let primaryButton = SwiftUI.Color(designHex: "#090624") + public static let primaryButtonText = SwiftUI.Color(designHex: "#FFFFFF") + public static let secondaryButton = SwiftUI.Color(designHex: "#FFFFFF") + public static let secondaryButtonText = SwiftUI.Color(designHex: "#090624") + public static let selectedButton = SwiftUI.Color(designHex: "#EAE4F5") + public static let selectedButtonText = SwiftUI.Color(designHex: "#500AB2") + public static let danger = SwiftUI.Color(designHex: "#DC2626") + public static let dangerSoft = SwiftUI.Color(designHex: "#FEF2F2") + public static let dangerText = SwiftUI.Color(designHex: "#FFFFFF") + public static let success = SwiftUI.Color(designHex: "#047857") + public static let successSoft = SwiftUI.Color(designHex: "#ECFDF5") + public static let warning = SwiftUI.Color(designHex: "#B45309") + public static let warningSoft = SwiftUI.Color(designHex: "#FFF7ED") + public static let info = SwiftUI.Color(designHex: "#500AB2") + public static let infoSoft = SwiftUI.Color(designHex: "#F6F3FB") + } + + public enum Typography { + public static let family = "Fustat" + public static let heading = Font.custom(family, size: 24).weight(.bold) + public static let body = Font.custom(family, size: 16).weight(.medium) + public static let caption = Font.custom(family, size: 14).weight(.medium) + public static let subtle = Font.custom(family, size: 12).weight(.medium) + public static let button = Font.custom(family, size: 14).weight(.bold) + } + + public enum Spacing { + public static let xsmall: CGFloat = 4 + public static let small: CGFloat = 8 + public static let medium: CGFloat = 12 + public static let large: CGFloat = 16 + public static let xlarge: CGFloat = 24 + } + + public enum Radius { + public static let button: CGFloat = 12 + public static let buttonLarge: CGFloat = 12 + public static let input: CGFloat = 12 + public static let badge: CGFloat = 999 + public static let card: CGFloat = 24 + } + + public enum Stroke { + public static let defaultWidth: CGFloat = 1 + public static let boxWidth: CGFloat = 1 + public static let labelWidth: CGFloat = 1 + public static let buttonWidth: CGFloat = 1 + } +} + +public enum DesignButtonVariant: Sendable { + case primary + case secondary + case selected + case dangerPrimary + case dangerSecondary +} + +public enum DesignButtonSize: Sendable { + case small + case medium + case large +} + +public enum DesignBadgeVariant: Sendable { + case info + case neutral + case success + case warning + case danger +} + +public enum DesignTextVariant: Sendable { + case title + case body + case caption + case subtle + case error +} + +public struct DesignButtonStyle: ButtonStyle { + private let variant: DesignButtonVariant + private let size: DesignButtonSize + private let fillsWidth: Bool + + public init( + variant: DesignButtonVariant = .primary, + size: DesignButtonSize = .medium, + fillsWidth: Bool = false + ) { + self.variant = variant + self.size = size + self.fillsWidth = fillsWidth + } + + public func makeBody(configuration: Configuration) -> some View { + configuration.label + .font(font) + .lineLimit(1) + .minimumScaleFactor(0.85) + .padding(.horizontal, horizontalPadding) + .frame(minHeight: height) + .frame(maxWidth: fillsWidth ? .infinity : nil) + .background(configuration.isPressed ? pressedBackground : background) + .foregroundStyle(foreground) + .overlay( + RoundedRectangle(cornerRadius: cornerRadius) + .stroke(borderColor, lineWidth: borderWidth) + ) + .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) + .contentShape(RoundedRectangle(cornerRadius: cornerRadius)) + } + + private var font: Font { + switch size { + case .small: Font.custom(DesignTokens.Typography.family, size: 12).weight(.bold) + case .medium: Font.custom(DesignTokens.Typography.family, size: 14).weight(.bold) + case .large: Font.custom(DesignTokens.Typography.family, size: 16).weight(.bold) + } + } + + private var height: CGFloat { + switch size { + case .small: 30 + case .medium: 32 + case .large: 40 + } + } + + private var horizontalPadding: CGFloat { + switch size { + case .small: 8 + case .medium: 12 + case .large: 12 + } + } + + private var cornerRadius: CGFloat { + switch size { + case .large: DesignTokens.Radius.buttonLarge + case .medium, .small: DesignTokens.Radius.button + } + } + + private var background: SwiftUI.Color { + switch variant { + case .primary: DesignTokens.Color.primaryButton + case .secondary: DesignTokens.Color.secondaryButton + case .selected: DesignTokens.Color.selectedButton + case .dangerPrimary: DesignTokens.Color.danger + case .dangerSecondary: DesignTokens.Color.dangerSoft + } + } + + private var pressedBackground: SwiftUI.Color { + switch variant { + case .primary, .secondary: DesignTokens.Color.hover + case .selected: DesignTokens.Color.selectedButton + case .dangerPrimary: DesignTokens.Color.danger + case .dangerSecondary: DesignTokens.Color.dangerSoft + } + } + + private var foreground: SwiftUI.Color { + switch variant { + case .primary: DesignTokens.Color.primaryButtonText + case .secondary: DesignTokens.Color.secondaryButtonText + case .selected: DesignTokens.Color.selectedButtonText + case .dangerPrimary: SwiftUI.Color.white + case .dangerSecondary: DesignTokens.Color.dangerText + } + } + + private var borderColor: SwiftUI.Color { + switch variant { + case .primary, .dangerPrimary: SwiftUI.Color.clear + case .secondary: DesignTokens.Color.border + case .selected: DesignTokens.Color.selectedButton + case .dangerSecondary: DesignTokens.Color.danger + } + } + + private var borderWidth: CGFloat { + switch variant { + case .primary, .dangerPrimary: 0 + case .secondary, .selected, .dangerSecondary: DesignTokens.Stroke.buttonWidth + } + } +} + +public struct DesignIconButton: View { + private let systemImage: String + private let accessibilityLabel: String + private let size: CGFloat + private let action: () -> Void + + public init( + systemImage: String, + accessibilityLabel: String, + size: CGFloat = 40, + action: @escaping () -> Void + ) { + self.systemImage = systemImage + self.accessibilityLabel = accessibilityLabel + self.size = size + self.action = action + } + + public var body: some View { + Button(action: action) { + Image(systemName: systemImage) + .font(.system(size: size * 0.38, weight: .semibold)) + .frame(width: size, height: size) + .background(DesignTokens.Color.surface) + .foregroundStyle(DesignTokens.Color.primaryText) + .overlay( + RoundedRectangle(cornerRadius: DesignTokens.Radius.buttonLarge) + .stroke(DesignTokens.Color.border, lineWidth: DesignTokens.Stroke.buttonWidth) + ) + .clipShape(RoundedRectangle(cornerRadius: DesignTokens.Radius.buttonLarge)) + } + .buttonStyle(.plain) + .accessibilityLabel(accessibilityLabel) + } +} + +public struct DesignText: View { + private let text: String + private let variant: DesignTextVariant + + public init(_ text: String, variant: DesignTextVariant = .body) { + self.text = text + self.variant = variant + } + + public var body: some View { + Text(text) + .font(font) + .foregroundStyle(color) + .fixedSize(horizontal: false, vertical: true) + } + + private var font: Font { + switch variant { + case .title: DesignTokens.Typography.heading + case .body: DesignTokens.Typography.body + case .caption: DesignTokens.Typography.caption + case .subtle: DesignTokens.Typography.subtle + case .error: DesignTokens.Typography.caption + } + } + + private var color: SwiftUI.Color { + switch variant { + case .title, .body: DesignTokens.Color.primaryText + case .caption, .subtle: DesignTokens.Color.secondaryText + case .error: DesignTokens.Color.danger + } + } +} + +public struct DesignToggle: View { + private let title: String + @Binding private var isOn: Bool + + public init(_ title: String, isOn: Binding) { + self.title = title + self._isOn = isOn + } + + public var body: some View { + Toggle(title, isOn: $isOn) + .font(DesignTokens.Typography.caption) + .foregroundStyle(DesignTokens.Color.primaryText) + .tint(DesignTokens.Color.brand) + } +} + +public struct DesignBadge: View { + private let text: String + private let variant: DesignBadgeVariant + + public init(_ text: String, variant: DesignBadgeVariant = .neutral) { + self.text = text + self.variant = variant + } + + public var body: some View { + Text(text) + .font(.custom(DesignTokens.Typography.family, size: 12).weight(.medium)) + .foregroundStyle(foregroundColor) + .padding(.horizontal, 10) + .padding(.vertical, 3) + .background(backgroundColor) + .overlay( + Capsule() + .stroke(borderColor, lineWidth: DesignTokens.Stroke.labelWidth) + ) + .clipShape(Capsule()) + } + + private var backgroundColor: SwiftUI.Color { + switch variant { + case .info: DesignTokens.Color.infoSoft + case .neutral: DesignTokens.Color.secondarySurface + case .success: DesignTokens.Color.successSoft + case .warning: DesignTokens.Color.warningSoft + case .danger: DesignTokens.Color.dangerSoft + } + } + + private var foregroundColor: SwiftUI.Color { + switch variant { + case .info: DesignTokens.Color.info + case .neutral: DesignTokens.Color.primaryText + case .success: DesignTokens.Color.success + case .warning: DesignTokens.Color.warning + case .danger: DesignTokens.Color.danger + } + } + + private var borderColor: SwiftUI.Color { + switch variant { + case .info: DesignTokens.Color.info + case .neutral: DesignTokens.Color.border + case .success: DesignTokens.Color.success + case .warning: DesignTokens.Color.warning + case .danger: DesignTokens.Color.danger + } + } +} + +public struct DesignCard: View { + private let content: () -> Content + + public init(@ViewBuilder content: @escaping () -> Content) { + self.content = content + } + + public var body: some View { + VStack(alignment: .leading, spacing: DesignTokens.Spacing.large) { + content() + } + .padding(16) + .frame(maxWidth: .infinity, alignment: .leading) + .background(DesignTokens.Color.surface) + .overlay( + RoundedRectangle(cornerRadius: DesignTokens.Radius.card) + .stroke(DesignTokens.Color.headerBorder, lineWidth: DesignTokens.Stroke.boxWidth) + ) + .clipShape(RoundedRectangle(cornerRadius: DesignTokens.Radius.card)) + } +} + +public struct DesignTextField: View { + private let title: String + @Binding private var text: String + private let isDestructive: Bool + + public init(_ title: String, text: Binding, isDestructive: Bool = false) { + self.title = title + self._text = text + self.isDestructive = isDestructive + } + + public var body: some View { + TextField(title, text: $text) + .font(DesignTokens.Typography.caption) + .foregroundStyle(DesignTokens.Color.primaryText) + .padding(.horizontal, 12) + .frame(minHeight: 40) + .background(DesignTokens.Color.secondarySurface) + .overlay( + RoundedRectangle(cornerRadius: DesignTokens.Radius.input) + .stroke(isDestructive ? DesignTokens.Color.danger : DesignTokens.Color.border, lineWidth: DesignTokens.Stroke.buttonWidth) + ) + .clipShape(RoundedRectangle(cornerRadius: DesignTokens.Radius.input)) + } +} diff --git a/Examples/trails-actions/styling.gen.swift b/Examples/trails-actions/styling.gen.swift index 058fb2d..dddc59d 100644 --- a/Examples/trails-actions/styling.gen.swift +++ b/Examples/trails-actions/styling.gen.swift @@ -1,36 +1,123 @@ -// Generated by oms-sdk-design-system. Do not edit manually. +// Generated by design-architect. Do not edit manually. +// Theme: OMS (oms) // swiftlint:disable all import SwiftUI -public enum OMSButtonVariant: Sendable { +private extension Color { + init(designHex hex: String) { + let value = hex.trimmingCharacters(in: CharacterSet(charactersIn: "#")) + let scanner = Scanner(string: value) + var rgba: UInt64 = 0 + scanner.scanHexInt64(&rgba) + + let red: Double + let green: Double + let blue: Double + let alpha: Double + + if value.count == 8 { + red = Double((rgba >> 24) & 0xff) / 255 + green = Double((rgba >> 16) & 0xff) / 255 + blue = Double((rgba >> 8) & 0xff) / 255 + alpha = Double(rgba & 0xff) / 255 + } else { + red = Double((rgba >> 16) & 0xff) / 255 + green = Double((rgba >> 8) & 0xff) / 255 + blue = Double(rgba & 0xff) / 255 + alpha = 1 + } + + self.init(.sRGB, red: red, green: green, blue: blue, opacity: alpha) + } +} + +public enum DesignTheme { + public static let id = "oms" + public static let name = "OMS" + public static let description = "Light financial product theme for OMS dashboards." +} + +public enum DesignTokens { + public enum Color { + public static let page = SwiftUI.Color(designHex: "#F6F6FC") + public static let surface = SwiftUI.Color(designHex: "#FFFFFF") + public static let secondarySurface = SwiftUI.Color(designHex: "#F6F6FC") + public static let header = SwiftUI.Color(designHex: "#FFFFFF") + public static let headerBorder = SwiftUI.Color(designHex: "#DFE3F0") + public static let footer = SwiftUI.Color(designHex: "#FFFFFF") + public static let primaryText = SwiftUI.Color(designHex: "#141635") + public static let secondaryText = SwiftUI.Color(designHex: "#64708F") + public static let placeholderText = SwiftUI.Color(designHex: "#929EBA") + public static let border = SwiftUI.Color(designHex: "#C8CFE1") + public static let focusRing = SwiftUI.Color(designHex: "#B78EEF") + public static let brand = SwiftUI.Color(designHex: "#670DE5") + public static let hover = SwiftUI.Color(designHex: "#F6F6FC") + public static let primaryButton = SwiftUI.Color(designHex: "#090624") + public static let primaryButtonText = SwiftUI.Color(designHex: "#FFFFFF") + public static let secondaryButton = SwiftUI.Color(designHex: "#FFFFFF") + public static let secondaryButtonText = SwiftUI.Color(designHex: "#090624") + public static let selectedButton = SwiftUI.Color(designHex: "#EAE4F5") + public static let selectedButtonText = SwiftUI.Color(designHex: "#500AB2") + public static let danger = SwiftUI.Color(designHex: "#DC2626") + public static let dangerSoft = SwiftUI.Color(designHex: "#FEF2F2") + public static let dangerText = SwiftUI.Color(designHex: "#FFFFFF") + public static let success = SwiftUI.Color(designHex: "#047857") + public static let successSoft = SwiftUI.Color(designHex: "#ECFDF5") + public static let warning = SwiftUI.Color(designHex: "#B45309") + public static let warningSoft = SwiftUI.Color(designHex: "#FFF7ED") + public static let info = SwiftUI.Color(designHex: "#500AB2") + public static let infoSoft = SwiftUI.Color(designHex: "#F6F3FB") + } + + public enum Typography { + public static let family = "Fustat" + public static let heading = Font.custom(family, size: 24).weight(.bold) + public static let body = Font.custom(family, size: 16).weight(.medium) + public static let caption = Font.custom(family, size: 14).weight(.medium) + public static let subtle = Font.custom(family, size: 12).weight(.medium) + public static let button = Font.custom(family, size: 14).weight(.bold) + } + + public enum Spacing { + public static let xsmall: CGFloat = 4 + public static let small: CGFloat = 8 + public static let medium: CGFloat = 12 + public static let large: CGFloat = 16 + public static let xlarge: CGFloat = 24 + } + + public enum Radius { + public static let button: CGFloat = 12 + public static let buttonLarge: CGFloat = 12 + public static let input: CGFloat = 12 + public static let badge: CGFloat = 999 + public static let card: CGFloat = 24 + } + + public enum Stroke { + public static let defaultWidth: CGFloat = 1 + public static let boxWidth: CGFloat = 1 + public static let labelWidth: CGFloat = 1 + public static let buttonWidth: CGFloat = 1 + } +} + +public enum DesignButtonVariant: Sendable { case primary case secondary + case selected case dangerPrimary case dangerSecondary } -public enum OMSButtonSize: Sendable { +public enum DesignButtonSize: Sendable { case small case medium case large } -public enum OMSLabelVariant: Sendable { - case title - case body - case caption - case subtle - case error -} - -public enum OMSFieldState: Sendable { - case normal - case error(String) - case disabled -} - -public enum OMSBadgeVariant: Sendable { +public enum DesignBadgeVariant: Sendable { case info case neutral case success @@ -38,80 +125,22 @@ public enum OMSBadgeVariant: Sendable { case danger } -public enum OMSAlertVariant: Sendable { - case info - case success - case warning - case danger -} - -public enum OMSTokens { - public enum Color { - public static let slate50 = SwiftUI.Color(red: 0.96, green: 0.96, blue: 0.99) - public static let slate100 = SwiftUI.Color(red: 0.93, green: 0.94, blue: 0.98) - public static let slate200 = SwiftUI.Color(red: 0.87, green: 0.89, blue: 0.94) - public static let slate300 = SwiftUI.Color(red: 0.78, green: 0.81, blue: 0.88) - public static let slate400 = SwiftUI.Color(red: 0.57, green: 0.62, blue: 0.73) - public static let slate500 = SwiftUI.Color(red: 0.39, green: 0.44, blue: 0.56) - public static let slate800 = SwiftUI.Color(red: 0.13, green: 0.15, blue: 0.27) - public static let slate900 = SwiftUI.Color(red: 0.08, green: 0.09, blue: 0.21) - public static let slate950 = SwiftUI.Color(red: 0.04, green: 0.02, blue: 0.14) - public static let purple50 = SwiftUI.Color(red: 0.96, green: 0.95, blue: 0.98) - public static let purple100 = SwiftUI.Color(red: 0.92, green: 0.89, blue: 0.96) - public static let purple200 = SwiftUI.Color(red: 0.84, green: 0.77, blue: 0.95) - public static let purple300 = SwiftUI.Color(red: 0.72, green: 0.56, blue: 0.94) - public static let purple500 = SwiftUI.Color(red: 0.40, green: 0.05, blue: 0.90) - public static let purple700 = SwiftUI.Color(red: 0.25, green: 0.04, blue: 0.56) - public static let red50 = SwiftUI.Color(red: 1.00, green: 0.95, blue: 0.95) - public static let red100 = SwiftUI.Color(red: 1.00, green: 0.89, blue: 0.89) - public static let red200 = SwiftUI.Color(red: 1.00, green: 0.79, blue: 0.79) - public static let red400 = SwiftUI.Color(red: 0.97, green: 0.44, blue: 0.44) - public static let red500 = SwiftUI.Color(red: 0.94, green: 0.27, blue: 0.27) - public static let red600 = SwiftUI.Color(red: 0.86, green: 0.15, blue: 0.15) - public static let red700 = SwiftUI.Color(red: 0.73, green: 0.11, blue: 0.11) - public static let red800 = SwiftUI.Color(red: 0.60, green: 0.11, blue: 0.11) - public static let emerald50 = SwiftUI.Color(red: 0.93, green: 0.99, blue: 0.96) - public static let emerald200 = SwiftUI.Color(red: 0.65, green: 0.95, blue: 0.82) - public static let emerald700 = SwiftUI.Color(red: 0.02, green: 0.47, blue: 0.34) - public static let orange50 = SwiftUI.Color(red: 1.00, green: 0.97, blue: 0.93) - public static let orange200 = SwiftUI.Color(red: 1.00, green: 0.84, blue: 0.67) - public static let amber700 = SwiftUI.Color(red: 0.71, green: 0.33, blue: 0.04) - public static let surface = SwiftUI.Color.white - public static let page = slate50 - public static let ink = slate900 - public static let mutedInk = slate500 - public static let border = slate300 - public static let brand = purple500 - public static let success = emerald700 - public static let warning = amber700 - public static let danger = red700 - } - - public enum Radius { - public static let button: CGFloat = 8 - public static let buttonLarge: CGFloat = 12 - public static let input: CGFloat = 12 - public static let badge: CGFloat = 16 - public static let card: CGFloat = 24 - } - - public enum Spacing { - public static let xsmall: CGFloat = 4 - public static let small: CGFloat = 8 - public static let medium: CGFloat = 12 - public static let large: CGFloat = 16 - public static let xlarge: CGFloat = 24 - } +public enum DesignTextVariant: Sendable { + case title + case body + case caption + case subtle + case error } -public struct OMSButtonStyle: ButtonStyle { - private let variant: OMSButtonVariant - private let size: OMSButtonSize +public struct DesignButtonStyle: ButtonStyle { + private let variant: DesignButtonVariant + private let size: DesignButtonSize private let fillsWidth: Bool public init( - variant: OMSButtonVariant = .primary, - size: OMSButtonSize = .medium, + variant: DesignButtonVariant = .primary, + size: DesignButtonSize = .medium, fillsWidth: Bool = false ) { self.variant = variant @@ -123,15 +152,15 @@ public struct OMSButtonStyle: ButtonStyle { configuration.label .font(font) .lineLimit(1) - .minimumScaleFactor(0.9) + .minimumScaleFactor(0.85) .padding(.horizontal, horizontalPadding) .frame(minHeight: height) .frame(maxWidth: fillsWidth ? .infinity : nil) - .background(backgroundColor(isPressed: configuration.isPressed)) - .foregroundStyle(foregroundColor) + .background(configuration.isPressed ? pressedBackground : background) + .foregroundStyle(foreground) .overlay( RoundedRectangle(cornerRadius: cornerRadius) - .stroke(borderColor, lineWidth: borderColor == .clear ? 0 : 1) + .stroke(borderColor, lineWidth: borderWidth) ) .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) .contentShape(RoundedRectangle(cornerRadius: cornerRadius)) @@ -139,108 +168,82 @@ public struct OMSButtonStyle: ButtonStyle { private var font: Font { switch size { - case .small: - return .custom("Fustat", size: 12).weight(.bold) - case .medium: - return .custom("Fustat", size: 14).weight(.bold) - case .large: - return .custom("Fustat", size: 16).weight(.bold) + case .small: Font.custom(DesignTokens.Typography.family, size: 12).weight(.bold) + case .medium: Font.custom(DesignTokens.Typography.family, size: 14).weight(.bold) + case .large: Font.custom(DesignTokens.Typography.family, size: 16).weight(.bold) } } private var height: CGFloat { switch size { - case .small: - return 30 - case .medium: - return 32 - case .large: - return 40 + case .small: 30 + case .medium: 32 + case .large: 40 } } private var horizontalPadding: CGFloat { switch size { - case .small: - return 8 - case .medium: - return 12 - case .large: - return 12 + case .small: 8 + case .medium: 12 + case .large: 12 } } private var cornerRadius: CGFloat { switch size { - case .large: - return OMSTokens.Radius.buttonLarge - case .medium, .small: - return OMSTokens.Radius.button + case .large: DesignTokens.Radius.buttonLarge + case .medium, .small: DesignTokens.Radius.button } } - private func backgroundColor(isPressed: Bool) -> SwiftUI.Color { + private var background: SwiftUI.Color { switch variant { - case .primary: - return isPressed ? OMSTokens.Color.purple700 : OMSTokens.Color.slate950 - case .secondary: - return isPressed ? OMSTokens.Color.purple100 : OMSTokens.Color.surface - case .dangerPrimary: - return isPressed ? OMSTokens.Color.red800 : OMSTokens.Color.red600 - case .dangerSecondary: - return isPressed ? OMSTokens.Color.red100 : OMSTokens.Color.surface + case .primary: DesignTokens.Color.primaryButton + case .secondary: DesignTokens.Color.secondaryButton + case .selected: DesignTokens.Color.selectedButton + case .dangerPrimary: DesignTokens.Color.danger + case .dangerSecondary: DesignTokens.Color.dangerSoft } } - private var foregroundColor: SwiftUI.Color { + private var pressedBackground: SwiftUI.Color { switch variant { - case .primary, .dangerPrimary: - return .white - case .secondary, .dangerSecondary: - return OMSTokens.Color.slate950 + case .primary, .secondary: DesignTokens.Color.hover + case .selected: DesignTokens.Color.selectedButton + case .dangerPrimary: DesignTokens.Color.danger + case .dangerSecondary: DesignTokens.Color.dangerSoft } } - private var borderColor: SwiftUI.Color { + private var foreground: SwiftUI.Color { switch variant { - case .primary, .dangerPrimary: - return .clear - case .secondary: - return OMSTokens.Color.slate500 - case .dangerSecondary: - return OMSTokens.Color.red600 + case .primary: DesignTokens.Color.primaryButtonText + case .secondary: DesignTokens.Color.secondaryButtonText + case .selected: DesignTokens.Color.selectedButtonText + case .dangerPrimary: SwiftUI.Color.white + case .dangerSecondary: DesignTokens.Color.dangerText } } -} - -public struct OMSButton: View { - private let variant: OMSButtonVariant - private let size: OMSButtonSize - private let fillsWidth: Bool - private let text: String - private let action: () -> Void - public init( - _ text: String, - variant: OMSButtonVariant = .primary, - size: OMSButtonSize = .medium, - fillsWidth: Bool = false, - action: @escaping () -> Void - ) { - self.text = text - self.variant = variant - self.size = size - self.fillsWidth = fillsWidth - self.action = action + private var borderColor: SwiftUI.Color { + switch variant { + case .primary, .dangerPrimary: SwiftUI.Color.clear + case .secondary: DesignTokens.Color.border + case .selected: DesignTokens.Color.selectedButton + case .dangerSecondary: DesignTokens.Color.danger + } } - public var body: some View { - Button(text, action: action) - .buttonStyle(OMSButtonStyle(variant: variant, size: size, fillsWidth: fillsWidth)) + private var borderWidth: CGFloat { + switch variant { + case .primary, .dangerPrimary: 0 + case .secondary, .selected, .dangerSecondary: DesignTokens.Stroke.buttonWidth + } } } -public struct OMSIconButton: View { +public struct DesignIconButton: View { private let systemImage: String private let accessibilityLabel: String private let size: CGFloat @@ -263,43 +266,24 @@ public struct OMSIconButton: View { Image(systemName: systemImage) .font(.system(size: size * 0.38, weight: .semibold)) .frame(width: size, height: size) - .background(OMSTokens.Color.surface) - .foregroundStyle(OMSTokens.Color.slate950) - .overlay(RoundedRectangle(cornerRadius: OMSTokens.Radius.buttonLarge).stroke(OMSTokens.Color.slate300, lineWidth: 1)) - .clipShape(RoundedRectangle(cornerRadius: OMSTokens.Radius.buttonLarge)) + .background(DesignTokens.Color.surface) + .foregroundStyle(DesignTokens.Color.primaryText) + .overlay( + RoundedRectangle(cornerRadius: DesignTokens.Radius.buttonLarge) + .stroke(DesignTokens.Color.border, lineWidth: DesignTokens.Stroke.buttonWidth) + ) + .clipShape(RoundedRectangle(cornerRadius: DesignTokens.Radius.buttonLarge)) } .buttonStyle(.plain) .accessibilityLabel(accessibilityLabel) } } -public struct OMSIconLabelButton: View { - private let systemImage: String +public struct DesignText: View { private let text: String - private let action: () -> Void - - public init(systemImage: String, text: String, action: @escaping () -> Void) { - self.systemImage = systemImage - self.text = text - self.action = action - } - - public var body: some View { - VStack(spacing: OMSTokens.Spacing.small) { - OMSIconButton(systemImage: systemImage, accessibilityLabel: text, size: 40, action: action) - Text(text) - .font(.custom("Fustat", size: 12).weight(.medium)) - .foregroundStyle(OMSTokens.Color.slate900) - .multilineTextAlignment(.center) - } - } -} + private let variant: DesignTextVariant -public struct OMSLabel: View { - private let text: String - private let variant: OMSLabelVariant - - public init(_ text: String, variant: OMSLabelVariant = .body) { + public init(_ text: String, variant: DesignTextVariant = .body) { self.text = text self.variant = variant } @@ -313,139 +297,24 @@ public struct OMSLabel: View { private var font: Font { switch variant { - case .title: - return .custom("Fustat", size: 24).weight(.bold) - case .body: - return .custom("Fustat", size: 16).weight(.medium) - case .caption: - return .custom("Fustat", size: 14).weight(.medium) - case .subtle: - return .custom("Fustat", size: 12).weight(.medium) - case .error: - return .custom("Fustat", size: 14).weight(.medium) + case .title: DesignTokens.Typography.heading + case .body: DesignTokens.Typography.body + case .caption: DesignTokens.Typography.caption + case .subtle: DesignTokens.Typography.subtle + case .error: DesignTokens.Typography.caption } } private var color: SwiftUI.Color { switch variant { - case .title: - return OMSTokens.Color.slate950 - case .body: - return OMSTokens.Color.slate900 - case .caption, .subtle: - return OMSTokens.Color.slate500 - case .error: - return OMSTokens.Color.red700 - } - } -} - -public struct OMSInputField: View { - private let title: String - private let placeholder: String - private let helperText: String? - private let state: OMSFieldState - @Binding private var text: String - - public init( - _ title: String, - text: Binding, - placeholder: String = "", - helperText: String? = nil, - state: OMSFieldState = .normal - ) { - self.title = title - self._text = text - self.placeholder = placeholder - self.helperText = helperText - self.state = state - } - - public var body: some View { - VStack(alignment: .leading, spacing: OMSTokens.Spacing.xsmall) { - OMSLabel(title, variant: .caption) - TextField(placeholder, text: $text) - .disabled(isDisabled) - .padding(.horizontal, OMSTokens.Spacing.medium) - .font(.custom("Fustat", size: 14).weight(.medium)) - .frame(minHeight: 40) - .background(OMSTokens.Color.surface) - .foregroundStyle(OMSTokens.Color.slate900) - .overlay( - RoundedRectangle(cornerRadius: OMSTokens.Radius.input) - .stroke(borderColor, lineWidth: 1) - ) - .clipShape(RoundedRectangle(cornerRadius: OMSTokens.Radius.input)) - .opacity(isDisabled ? 0.5 : 1) - - if let message = messageText { - OMSLabel(message, variant: messageIsError ? .error : .caption) - } - } - } - - private var isDisabled: Bool { - if case .disabled = state { return true } - return false - } - - private var messageText: String? { - if case .error(let message) = state { return message } - return helperText - } - - private var messageIsError: Bool { - if case .error = state { return true } - return false - } - - private var borderColor: SwiftUI.Color { - switch state { - case .normal: - return OMSTokens.Color.slate300 - case .error: - return OMSTokens.Color.slate300 - case .disabled: - return OMSTokens.Color.slate300 + case .title, .body: DesignTokens.Color.primaryText + case .caption, .subtle: DesignTokens.Color.secondaryText + case .error: DesignTokens.Color.danger } } } -public struct OMSDropdown: View { - private let title: String - private let options: [Option] - @Binding private var selection: Option - - public init(_ title: String, selection: Binding