diff --git a/.DS_Store b/.DS_Store
index 5f0c85a..97f033e 100644
Binary files a/.DS_Store and b/.DS_Store differ
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index eefdda0..6914ea1 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -15,7 +15,8 @@
- [ ] `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
+- [ ] SDK demo app builds (`xcodebuild -project Examples/sdk-demo/oms-sdk-demo.xcodeproj -scheme oms-sdk-demo build CODE_SIGNING_ALLOWED=NO`)
+- [ ] Trails Actions demo app builds (`xcodebuild -project Examples/trails-actions/trails-actions.xcodeproj -scheme trails-actions build CODE_SIGNING_ALLOWED=NO`)
## Related
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7f0b1c7..636a208 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -22,3 +22,9 @@ jobs:
- name: Test
run: swift test
+
+ - name: Build SDK demo app
+ run: xcodebuild -project Examples/sdk-demo/oms-sdk-demo.xcodeproj -scheme oms-sdk-demo build CODE_SIGNING_ALLOWED=NO
+
+ - name: Build Trails Actions demo app
+ run: xcodebuild -project Examples/trails-actions/trails-actions.xcodeproj -scheme trails-actions build CODE_SIGNING_ALLOWED=NO
diff --git a/AGENTS.md b/AGENTS.md
index dcf22b4..63ccdf5 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -85,6 +85,8 @@ and target names in shell commands, for example `"Sources/Swift SDK"` and
`#expect`.
- `Examples/sdk-demo/oms-sdk-demo.xcodeproj` and
`Examples/sdk-demo/oms-sdk-demo/` contain the SwiftUI demo app.
+- `Examples/trails-actions/trails-actions.xcodeproj` and
+ `Examples/trails-actions/trails-actions/` contain the Trails Actions demo app.
- `README.md` is the user-facing guide; `API.md` is the detailed API reference.
## Common Commands
@@ -93,11 +95,13 @@ 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
+xcodebuild -list -project Examples/trails-actions/trails-actions.xcodeproj
+xcodebuild -project Examples/trails-actions/trails-actions.xcodeproj -scheme trails-actions build CODE_SIGNING_ALLOWED=NO
```
-Run `swift test` for SDK changes. For demo app changes, also build the Xcode
-project with the `oms-sdk-demo` scheme when feasible.
+Run `swift test` for SDK changes. For demo app changes, also build the relevant
+Xcode project with signing disabled when feasible.
## Testing
@@ -127,8 +131,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 demo app Xcode builds 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/Examples/sdk-demo/Info.plist b/Examples/sdk-demo/Info.plist
index a4b5603..924e0d2 100644
--- a/Examples/sdk-demo/Info.plist
+++ b/Examples/sdk-demo/Info.plist
@@ -57,5 +57,7 @@
UIInterfaceOrientationLandscapeLeft
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