mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
fix(ios): brand remaining SwiftUI controls
This commit is contained in:
@@ -49,6 +49,7 @@ struct AgentProDreamingDestination: View {
|
||||
self.dreamingPhasesCard
|
||||
}
|
||||
.padding(.vertical, 18)
|
||||
.font(OpenClawType.body)
|
||||
}
|
||||
.refreshable {
|
||||
await self.refresh()
|
||||
@@ -194,6 +195,7 @@ struct AgentProDreamingDestination: View {
|
||||
Task { await self.runDreamAction(action) }
|
||||
} label: {
|
||||
Label(action.title, systemImage: self.dreamActionBusy == action ? "hourglass" : action.icon)
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.controlSize(.small)
|
||||
|
||||
@@ -23,6 +23,7 @@ struct AgentProNodesDestination: View {
|
||||
self.nodesList
|
||||
}
|
||||
.padding(.vertical, 18)
|
||||
.font(OpenClawType.body)
|
||||
}
|
||||
.refreshable {
|
||||
await self.refresh()
|
||||
@@ -208,6 +209,7 @@ struct AgentProNodesDestination: View {
|
||||
self.nodeListCard(title: "Tags", values: entry.tags ?? [])
|
||||
}
|
||||
.padding(.vertical, 18)
|
||||
.font(OpenClawType.body)
|
||||
}
|
||||
.safeAreaPadding(.bottom, OpenClawProMetric.bottomScrollInset)
|
||||
}
|
||||
|
||||
@@ -98,6 +98,7 @@ extension AgentProTab {
|
||||
Task { await self.runCronJob(job) }
|
||||
} label: {
|
||||
Label("Run", systemImage: "play.fill")
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
}
|
||||
.disabled(busy || !self.liveGatewayConnected)
|
||||
|
||||
@@ -105,6 +106,7 @@ extension AgentProTab {
|
||||
Task { await self.setCronJob(job, enabled: !job.enabled) }
|
||||
} label: {
|
||||
Label(job.enabled ? "Pause" : "Enable", systemImage: job.enabled ? "pause.fill" : "checkmark")
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
}
|
||||
.disabled(busy || !self.liveGatewayConnected)
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ extension AgentProTab {
|
||||
.refreshable {
|
||||
await self.refreshOverview(force: true)
|
||||
}
|
||||
.font(OpenClawType.body)
|
||||
.toolbar {
|
||||
if let headerLeadingAction {
|
||||
ToolbarItem(placement: .topBarLeading) {
|
||||
@@ -70,6 +71,7 @@ extension AgentProTab {
|
||||
self.skillsList
|
||||
}
|
||||
.padding(.vertical, 18)
|
||||
.font(OpenClawType.body)
|
||||
}
|
||||
.refreshable {
|
||||
await self.refreshOverview(force: true)
|
||||
@@ -113,6 +115,7 @@ extension AgentProTab {
|
||||
self.cronJobsList(limit: nil)
|
||||
}
|
||||
.padding(.vertical, 18)
|
||||
.font(OpenClawType.body)
|
||||
}
|
||||
.refreshable {
|
||||
await self.refreshOverview(force: true)
|
||||
@@ -142,6 +145,7 @@ extension AgentProTab {
|
||||
self.usageDailyList
|
||||
}
|
||||
.padding(.vertical, 18)
|
||||
.font(OpenClawType.body)
|
||||
}
|
||||
.refreshable {
|
||||
await self.refreshOverview(force: true)
|
||||
|
||||
@@ -21,18 +21,27 @@ extension AgentProTab {
|
||||
}
|
||||
|
||||
HStack(spacing: 8) {
|
||||
Button("Enable All") {
|
||||
Button {
|
||||
Task { await self.enableAllSkills() }
|
||||
} label: {
|
||||
Text("Enable All")
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
}
|
||||
.disabled(self.skillMutationBusy)
|
||||
|
||||
Button("Disable All", role: .destructive) {
|
||||
Button(role: .destructive) {
|
||||
Task { await self.disableAllSkills() }
|
||||
} label: {
|
||||
Text("Disable All")
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
}
|
||||
.disabled(self.skillMutationBusy)
|
||||
|
||||
Button("Reset") {
|
||||
Button {
|
||||
Task { await self.resetSkillPolicy() }
|
||||
} label: {
|
||||
Text("Reset")
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
}
|
||||
.disabled(self.skillMutationBusy || self.agentSkillFilter == nil)
|
||||
}
|
||||
@@ -77,7 +86,9 @@ extension AgentProTab {
|
||||
}
|
||||
Picker("Status", selection: self.$skillStatusFilter) {
|
||||
ForEach(SkillStatusFilter.allCases) { filter in
|
||||
Text(filter.title).tag(filter)
|
||||
Text(filter.title)
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.tag(filter)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
@@ -381,8 +392,11 @@ extension AgentProTab {
|
||||
.navigationTitle("Skill")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Close") {
|
||||
Button {
|
||||
self.skillEditorSelection = nil
|
||||
} label: {
|
||||
Text("Close")
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -407,8 +421,11 @@ extension AgentProTab {
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Close") {
|
||||
Button {
|
||||
self.skillEditorSelection = nil
|
||||
} label: {
|
||||
Text("Close")
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -459,6 +476,7 @@ extension AgentProTab {
|
||||
Task { await self.saveSkillAPIKey(skill) }
|
||||
} label: {
|
||||
Label("Save key", systemImage: "key")
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.controlSize(.small)
|
||||
@@ -523,6 +541,7 @@ extension AgentProTab {
|
||||
Task { await self.installSkillRequirements(skill) }
|
||||
} label: {
|
||||
Label(install.label, systemImage: "wrench.and.screwdriver")
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.controlSize(.small)
|
||||
|
||||
@@ -48,6 +48,7 @@ struct IPadSidebarScreenChrome<Content: View>: View {
|
||||
self.content
|
||||
}
|
||||
.padding(.vertical, self.isCompactHeight ? 10 : 18)
|
||||
.font(OpenClawType.body)
|
||||
}
|
||||
.safeAreaPadding(.bottom, self.bottomScrollInset)
|
||||
}
|
||||
|
||||
@@ -71,8 +71,11 @@ struct IPadSkillWorkshopScreen: View {
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("Done") {
|
||||
Button {
|
||||
self.presentedProposalRoute = nil
|
||||
} label: {
|
||||
Text("Done")
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -118,6 +121,7 @@ struct IPadSkillWorkshopScreen: View {
|
||||
Task { await self.loadProposals(force: true) }
|
||||
} label: {
|
||||
Label("Refresh", systemImage: "arrow.clockwise")
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.controlSize(.small)
|
||||
@@ -177,6 +181,7 @@ struct IPadSkillWorkshopScreen: View {
|
||||
Task { await self.loadProposals(force: true) }
|
||||
} label: {
|
||||
Label("Refresh", systemImage: "arrow.clockwise")
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
@@ -226,12 +231,18 @@ struct IPadSkillWorkshopScreen: View {
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.foregroundStyle(.secondary)
|
||||
Menu {
|
||||
Button("Default agent") {
|
||||
Button {
|
||||
self.selectedAgentScopeID = ""
|
||||
} label: {
|
||||
Text("Default agent")
|
||||
.font(OpenClawType.subhead)
|
||||
}
|
||||
ForEach(self.agentScopeOptions, id: \.id) { option in
|
||||
Button(option.title) {
|
||||
Button {
|
||||
self.selectedAgentScopeID = option.id
|
||||
} label: {
|
||||
Text(option.title)
|
||||
.font(OpenClawType.subhead)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
@@ -347,42 +358,60 @@ struct IPadSkillWorkshopScreen: View {
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.contextMenu {
|
||||
Button("Inspect") {
|
||||
Button {
|
||||
self.selectProposal(
|
||||
proposal,
|
||||
opensSheet: true,
|
||||
forceInspect: true)
|
||||
} label: {
|
||||
Text("Inspect")
|
||||
.font(OpenClawType.subhead)
|
||||
}
|
||||
if proposal.status == "pending" {
|
||||
Button("Apply") {
|
||||
Button {
|
||||
Task { await self.run(.apply, proposal: proposal) }
|
||||
} label: {
|
||||
Text("Apply")
|
||||
.font(OpenClawType.subhead)
|
||||
}
|
||||
.disabled(!self.canApplyProposalMutations || self.busyAction != nil)
|
||||
Button("Reject", role: .destructive) {
|
||||
Button(role: .destructive) {
|
||||
Task { await self.run(.reject, proposal: proposal) }
|
||||
} label: {
|
||||
Text("Reject")
|
||||
.font(OpenClawType.subhead)
|
||||
}
|
||||
.disabled(!self.canApplyProposalMutations || self.busyAction != nil)
|
||||
}
|
||||
}
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
||||
if proposal.status == "pending" {
|
||||
Button("Apply") {
|
||||
Button {
|
||||
Task { await self.run(.apply, proposal: proposal) }
|
||||
} label: {
|
||||
Text("Apply")
|
||||
.font(OpenClawType.subhead)
|
||||
}
|
||||
.tint(OpenClawBrand.ok)
|
||||
.disabled(!self.canApplyProposalMutations || self.busyAction != nil)
|
||||
Button("Reject", role: .destructive) {
|
||||
Button(role: .destructive) {
|
||||
Task { await self.run(.reject, proposal: proposal) }
|
||||
} label: {
|
||||
Text("Reject")
|
||||
.font(OpenClawType.subhead)
|
||||
}
|
||||
.disabled(!self.canApplyProposalMutations || self.busyAction != nil)
|
||||
}
|
||||
}
|
||||
.swipeActions(edge: .leading, allowsFullSwipe: true) {
|
||||
Button("Inspect") {
|
||||
Button {
|
||||
self.selectProposal(
|
||||
proposal,
|
||||
opensSheet: true,
|
||||
forceInspect: true)
|
||||
} label: {
|
||||
Text("Inspect")
|
||||
.font(OpenClawType.subhead)
|
||||
}
|
||||
.tint(OpenClawBrand.accent)
|
||||
}
|
||||
@@ -482,6 +511,7 @@ struct IPadSkillWorkshopScreen: View {
|
||||
Task { await self.run(.apply, proposal: proposal) }
|
||||
} label: {
|
||||
Label("Apply", systemImage: "checkmark.circle")
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.frame(maxWidth: self.isCompactWidth ? .infinity : nil)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
@@ -494,6 +524,7 @@ struct IPadSkillWorkshopScreen: View {
|
||||
Task { await self.run(.reject, proposal: proposal) }
|
||||
} label: {
|
||||
Label("Reject", systemImage: "xmark.circle")
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.frame(maxWidth: self.isCompactWidth ? .infinity : nil)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
@@ -506,6 +537,7 @@ struct IPadSkillWorkshopScreen: View {
|
||||
Task { await self.inspect(proposalID: proposal.id, force: true) }
|
||||
} label: {
|
||||
Label("Inspect", systemImage: "doc.text.magnifyingglass")
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.frame(maxWidth: self.isCompactWidth ? .infinity : nil)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
@@ -976,6 +1008,7 @@ private struct IPadSkillProposalKanbanCard: View {
|
||||
if self.proposal.status == "pending" {
|
||||
Button(action: self.apply) {
|
||||
Image(systemName: "checkmark.circle")
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
}
|
||||
.accessibilityLabel("Apply Proposal")
|
||||
.buttonStyle(.bordered)
|
||||
@@ -984,6 +1017,7 @@ private struct IPadSkillProposalKanbanCard: View {
|
||||
|
||||
Button(role: .destructive, action: self.reject) {
|
||||
Image(systemName: "xmark.circle")
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
}
|
||||
.accessibilityLabel("Reject Proposal")
|
||||
.buttonStyle(.bordered)
|
||||
@@ -993,6 +1027,7 @@ private struct IPadSkillProposalKanbanCard: View {
|
||||
|
||||
Button(action: self.inspect) {
|
||||
Image(systemName: "doc.text.magnifyingglass")
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
}
|
||||
.accessibilityLabel("Inspect Proposal")
|
||||
.buttonStyle(.bordered)
|
||||
@@ -1006,12 +1041,21 @@ private struct IPadSkillProposalKanbanCard: View {
|
||||
in: RoundedRectangle(cornerRadius: OpenClawRadius.xs, style: .continuous))
|
||||
.contentShape(Rectangle())
|
||||
.contextMenu {
|
||||
Button("Inspect", action: self.inspect)
|
||||
Button(action: self.inspect) {
|
||||
Text("Inspect")
|
||||
.font(OpenClawType.subhead)
|
||||
}
|
||||
if self.proposal.status == "pending" {
|
||||
Button("Apply", action: self.apply)
|
||||
.disabled(!self.canApplyProposalMutations || self.isBusy)
|
||||
Button("Reject", role: .destructive, action: self.reject)
|
||||
.disabled(!self.canApplyProposalMutations || self.isBusy)
|
||||
Button(action: self.apply) {
|
||||
Text("Apply")
|
||||
.font(OpenClawType.subhead)
|
||||
}
|
||||
.disabled(!self.canApplyProposalMutations || self.isBusy)
|
||||
Button(role: .destructive, action: self.reject) {
|
||||
Text("Reject")
|
||||
.font(OpenClawType.subhead)
|
||||
}
|
||||
.disabled(!self.canApplyProposalMutations || self.isBusy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ struct OpenClawDocsScreen: View {
|
||||
self.linkCard
|
||||
}
|
||||
.padding(.vertical, 18)
|
||||
.font(OpenClawType.body)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Docs")
|
||||
@@ -39,6 +40,7 @@ struct OpenClawDocsScreen: View {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button(action: gatewayAction) {
|
||||
Image(systemName: "antenna.radiowaves.left.and.right")
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
.accessibilityLabel("Gateway settings")
|
||||
}
|
||||
|
||||
@@ -47,9 +47,11 @@ struct ProSectionHeader: View {
|
||||
Spacer()
|
||||
if let actionTitle {
|
||||
if let action {
|
||||
Button(actionTitle, action: action)
|
||||
.font(OpenClawType.footnoteMedium)
|
||||
.foregroundStyle(OpenClawBrand.accent)
|
||||
Button(action: action) {
|
||||
Text(actionTitle)
|
||||
.font(OpenClawType.footnoteMedium)
|
||||
}
|
||||
.foregroundStyle(OpenClawBrand.accent)
|
||||
} else {
|
||||
Text(actionTitle)
|
||||
.font(OpenClawType.footnoteMedium)
|
||||
@@ -134,19 +136,23 @@ private struct OpenClawGlassButtonModifier: ViewModifier {
|
||||
if #available(iOS 26.0, *) {
|
||||
if self.prominent {
|
||||
content
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.buttonStyle(.glassProminent)
|
||||
.tint(self.tint ?? OpenClawBrand.accent)
|
||||
} else {
|
||||
content
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.buttonStyle(.glass)
|
||||
.tint(self.tint)
|
||||
}
|
||||
} else if self.prominent {
|
||||
content
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(self.tint ?? OpenClawBrand.accent)
|
||||
} else {
|
||||
content
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.buttonStyle(.bordered)
|
||||
.tint(self.tint)
|
||||
}
|
||||
@@ -362,14 +368,20 @@ struct OpenClawNoticeBanner: View {
|
||||
OpenClawGlassControlGroup {
|
||||
HStack(spacing: 10) {
|
||||
if let primaryActionTitle, let onPrimaryAction {
|
||||
Button(primaryActionTitle, action: onPrimaryAction)
|
||||
.openClawGlassButton(prominent: true)
|
||||
.controlSize(.small)
|
||||
Button(action: onPrimaryAction) {
|
||||
Text(primaryActionTitle)
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
}
|
||||
.openClawGlassButton(prominent: true)
|
||||
.controlSize(.small)
|
||||
}
|
||||
if let secondaryActionTitle, let onSecondaryAction {
|
||||
Button(secondaryActionTitle, action: onSecondaryAction)
|
||||
.openClawGlassButton()
|
||||
.controlSize(.small)
|
||||
Button(action: onSecondaryAction) {
|
||||
Text(secondaryActionTitle)
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
}
|
||||
.openClawGlassButton()
|
||||
.controlSize(.small)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -795,9 +807,11 @@ struct ProPanelHeader: View {
|
||||
.accessibilityLabel(self.actionAccessibilityLabel ?? actionTitle ?? self.title)
|
||||
.disabled(self.isActionDisabled)
|
||||
} else if let actionTitle {
|
||||
Button(actionTitle, action: action)
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.disabled(self.isActionDisabled)
|
||||
Button(action: action) {
|
||||
Text(actionTitle)
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
}
|
||||
.disabled(self.isActionDisabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -830,10 +844,12 @@ struct ProStatusRow: View {
|
||||
ProValuePill(value: value, color: self.color)
|
||||
}
|
||||
if let actionTitle, let action {
|
||||
Button(actionTitle, action: action)
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.buttonStyle(.bordered)
|
||||
.controlSize(.mini)
|
||||
Button(action: action) {
|
||||
Text(actionTitle)
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.controlSize(.mini)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ struct SettingsChannelsDestination: View {
|
||||
}
|
||||
self.channelsCard
|
||||
}
|
||||
.font(OpenClawType.body)
|
||||
.task(id: self.refreshID) {
|
||||
await self.loadChannels(force: false)
|
||||
}
|
||||
@@ -380,6 +381,7 @@ private struct SettingsChannelRow: View {
|
||||
private func accountRow(_ account: SettingsChannelAccount) -> some View {
|
||||
HStack(spacing: 10) {
|
||||
Image(systemName: account.running || account.connected ? "checkmark.circle.fill" : "circle")
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.foregroundStyle(account.color)
|
||||
.frame(width: 28, height: 28)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
@@ -393,22 +395,32 @@ private struct SettingsChannelRow: View {
|
||||
Spacer(minLength: 8)
|
||||
Menu {
|
||||
if account.running {
|
||||
Button("Stop") {
|
||||
Button {
|
||||
self.stop(account.id)
|
||||
} label: {
|
||||
Text("Stop")
|
||||
.font(OpenClawType.subhead)
|
||||
}
|
||||
} else {
|
||||
Button("Start") {
|
||||
Button {
|
||||
self.start(account.id)
|
||||
} label: {
|
||||
Text("Start")
|
||||
.font(OpenClawType.subhead)
|
||||
}
|
||||
.disabled(!account.configured || !account.enabled)
|
||||
}
|
||||
if account.linked {
|
||||
Button("Logout", role: .destructive) {
|
||||
Button(role: .destructive) {
|
||||
self.logout(account.id)
|
||||
} label: {
|
||||
Text("Logout")
|
||||
.font(OpenClawType.subhead)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: self.actionMenuIcon(account))
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.controlSize(.mini)
|
||||
|
||||
@@ -119,6 +119,7 @@ struct SettingsProTab: View {
|
||||
self.gatewaySection
|
||||
self.settingsListSection
|
||||
}
|
||||
.font(OpenClawType.body)
|
||||
.listStyle(.insetGrouped)
|
||||
.navigationTitle("Settings")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
@@ -216,9 +217,15 @@ struct SettingsProTab: View {
|
||||
.ignoresSafeArea()
|
||||
.navigationTitle("Scan QR Code")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.font(OpenClawType.body)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarLeading) {
|
||||
Button("Cancel") { self.showQRScanner = false }
|
||||
Button {
|
||||
self.showQRScanner = false
|
||||
} label: {
|
||||
Text("Cancel")
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -294,17 +301,23 @@ struct HostedPushRelayDisclosureSheet: View {
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.font(OpenClawType.body)
|
||||
}
|
||||
VStack(spacing: 10) {
|
||||
Button {
|
||||
self.dismiss()
|
||||
self.onContinue()
|
||||
} label: {
|
||||
Text("Continue").frame(maxWidth: .infinity)
|
||||
Text("Continue")
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
Button("Not Now", role: .cancel) {
|
||||
Button(role: .cancel) {
|
||||
self.dismiss()
|
||||
} label: {
|
||||
Text("Not Now")
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
@@ -241,6 +241,7 @@ extension SettingsProTab {
|
||||
}
|
||||
.padding(.top, 18)
|
||||
.padding(.bottom, OpenClawProMetric.bottomScrollInset)
|
||||
.font(OpenClawType.body)
|
||||
}
|
||||
}
|
||||
.navigationTitle(self.title(for: route))
|
||||
@@ -335,6 +336,7 @@ extension SettingsProTab {
|
||||
self.openNotificationsRouteFromApprovals()
|
||||
} label: {
|
||||
Label("Open Notifications", systemImage: "bell.badge")
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
@@ -369,6 +371,7 @@ extension SettingsProTab {
|
||||
Task { await self.appModel.resolvePendingExecApprovalPrompt(decision: "allow-once") }
|
||||
} label: {
|
||||
Label("Allow", systemImage: "checkmark")
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.disabled(self.appModel.pendingExecApprovalPromptResolving)
|
||||
@@ -380,6 +383,7 @@ extension SettingsProTab {
|
||||
}
|
||||
} label: {
|
||||
Label("Always", systemImage: "checkmark.shield")
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.disabled(self.appModel.pendingExecApprovalPromptResolving)
|
||||
@@ -389,6 +393,7 @@ extension SettingsProTab {
|
||||
Task { await self.appModel.resolvePendingExecApprovalPrompt(decision: "deny") }
|
||||
} label: {
|
||||
Label("Deny", systemImage: "xmark")
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.disabled(self.appModel.pendingExecApprovalPromptResolving)
|
||||
@@ -530,6 +535,7 @@ extension SettingsProTab {
|
||||
Label(
|
||||
self.notificationActionText,
|
||||
systemImage: self.notificationStatus.actionIcon)
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
@@ -710,9 +716,15 @@ extension SettingsProTab {
|
||||
}
|
||||
|
||||
Picker("Location", selection: self.$locationModeRaw) {
|
||||
Text("Off").tag(OpenClawLocationMode.off.rawValue)
|
||||
Text("While Using").tag(OpenClawLocationMode.whileUsing.rawValue)
|
||||
Text("Always").tag(OpenClawLocationMode.always.rawValue)
|
||||
Text("Off")
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.tag(OpenClawLocationMode.off.rawValue)
|
||||
Text("While Using")
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.tag(OpenClawLocationMode.whileUsing.rawValue)
|
||||
Text("Always")
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.tag(OpenClawLocationMode.always.rawValue)
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.disabled(self.isChangingLocationMode)
|
||||
@@ -793,12 +805,16 @@ extension SettingsProTab {
|
||||
Text("Default Agent")
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
Picker("Agent", selection: self.$selectedAgentPickerId) {
|
||||
Text("Default").tag("")
|
||||
Text("Default")
|
||||
.font(OpenClawType.subhead)
|
||||
.tag("")
|
||||
let defaultId = (self.appModel.gatewayDefaultAgentId ?? "")
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
ForEach(self.appModel.gatewayAgents.filter { $0.id != defaultId }, id: \.id) { agent in
|
||||
let name = (agent.name ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
Text(name.isEmpty ? agent.id : name).tag(agent.id)
|
||||
Text(name.isEmpty ? agent.id : name)
|
||||
.font(OpenClawType.subhead)
|
||||
.tag(agent.id)
|
||||
}
|
||||
}
|
||||
Text("Used for new Chat and Talk sessions.")
|
||||
@@ -817,6 +833,7 @@ extension SettingsProTab {
|
||||
TextField("Paste setup code", text: self.$setupCode)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
.font(OpenClawType.subhead)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
HStack(spacing: 10) {
|
||||
self.gatewayActionButton(
|
||||
@@ -893,6 +910,7 @@ extension SettingsProTab {
|
||||
ProgressView().controlSize(.small)
|
||||
} else {
|
||||
Text("Connect")
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
@@ -907,9 +925,11 @@ extension SettingsProTab {
|
||||
TextField("Host", text: self.$manualGatewayHost)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
.font(OpenClawType.subhead)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
TextField("Port", text: self.manualPortBinding)
|
||||
.keyboardType(.numberPad)
|
||||
.font(OpenClawType.subhead)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
self.settingsButtonToggle("Use TLS", isOn: self.$manualGatewayTLS)
|
||||
self.gatewayActionButton(
|
||||
@@ -934,13 +954,16 @@ extension SettingsProTab {
|
||||
SecureField("Gateway Auth Token", text: self.$gatewayToken)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
.font(OpenClawType.subhead)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
SecureField("Gateway Password", text: self.$gatewayPassword)
|
||||
.font(OpenClawType.subhead)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
Button(role: .destructive) {
|
||||
self.showResetOnboardingAlert = true
|
||||
} label: {
|
||||
Label("Reset Onboarding", systemImage: "arrow.counterclockwise")
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
@@ -966,7 +989,9 @@ extension SettingsProTab {
|
||||
.disabled(self.appModel.isAppleReviewDemoModeEnabled)
|
||||
Picker("Speech Language", selection: self.$talkSpeechLocale) {
|
||||
ForEach(TalkSpeechLocale.supportedOptions()) { option in
|
||||
Text(option.label).tag(option.id)
|
||||
Text(option.label)
|
||||
.font(OpenClawType.subhead)
|
||||
.tag(option.id)
|
||||
}
|
||||
}
|
||||
self.settingsToggle("Background Listening", isOn: self.$talkBackgroundEnabled)
|
||||
@@ -999,14 +1024,20 @@ extension SettingsProTab {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Picker("Provider", selection: self.talkProviderSelectionBinding) {
|
||||
ForEach(TalkModeProviderSelection.allCases) { option in
|
||||
Text(option.label).tag(option.rawValue)
|
||||
Text(option.label)
|
||||
.font(OpenClawType.subhead)
|
||||
.tag(option.rawValue)
|
||||
}
|
||||
}
|
||||
if self.shouldShowRealtimeVoicePicker {
|
||||
Picker("Realtime Voice", selection: self.talkRealtimeVoiceSelectionBinding) {
|
||||
Text("Gateway Default").tag("")
|
||||
Text("Gateway Default")
|
||||
.font(OpenClawType.subhead)
|
||||
.tag("")
|
||||
ForEach(TalkModeRealtimeVoiceSelection.voices, id: \.self) { voice in
|
||||
Text(TalkModeRealtimeVoiceSelection.label(for: voice)).tag(voice)
|
||||
Text(TalkModeRealtimeVoiceSelection.label(for: voice))
|
||||
.font(OpenClawType.subhead)
|
||||
.tag(voice)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1030,15 +1061,20 @@ extension SettingsProTab {
|
||||
var shareSettingsCard: some View {
|
||||
ProCard(radius: SettingsLayout.cardRadius) {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Toggle("Show Talk Control", isOn: self.$talkButtonEnabled)
|
||||
Toggle(isOn: self.$talkButtonEnabled) {
|
||||
Text("Show Talk Control")
|
||||
.font(OpenClawType.subhead)
|
||||
}
|
||||
TextField("Default Share Instruction", text: self.$defaultShareInstruction, axis: .vertical)
|
||||
.lineLimit(2...5)
|
||||
.textInputAutocapitalization(.sentences)
|
||||
.font(OpenClawType.subhead)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
Button {
|
||||
Task { await self.appModel.runSharePipelineSelfTest() }
|
||||
} label: {
|
||||
Label("Run Share Self-Test", systemImage: "checkmark.seal")
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
Text(self.appModel.lastShareEventText)
|
||||
@@ -1089,10 +1125,13 @@ extension SettingsProTab {
|
||||
isOn: Binding<Bool>,
|
||||
onChange: ((Bool) -> Void)? = nil) -> some View
|
||||
{
|
||||
Toggle(title, isOn: isOn)
|
||||
.onChange(of: isOn.wrappedValue) { _, enabled in
|
||||
onChange?(enabled)
|
||||
}
|
||||
Toggle(isOn: isOn) {
|
||||
Text(title)
|
||||
.font(OpenClawType.subhead)
|
||||
}
|
||||
.onChange(of: isOn.wrappedValue) { _, enabled in
|
||||
onChange?(enabled)
|
||||
}
|
||||
}
|
||||
|
||||
func settingsButtonToggle(
|
||||
|
||||
@@ -60,8 +60,11 @@ struct TalkProTab: View {
|
||||
.navigationTitle("Enable Talk")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Not Now") {
|
||||
Button {
|
||||
self.showPermissionPrompt = false
|
||||
} label: {
|
||||
Text("Not Now")
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,9 +60,12 @@ struct TalkRuntimeIssueDetailsSheet: View {
|
||||
.font(OpenClawType.monoFootnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.textSelection(.enabled)
|
||||
Button("Copy diagnostics") {
|
||||
Button {
|
||||
UIPasteboard.general.string = self.issue.technicalDetails
|
||||
self.copyFeedback = "Copied diagnostics"
|
||||
} label: {
|
||||
Text("Copy diagnostics")
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,15 +82,21 @@ struct TalkRuntimeIssueDetailsSheet: View {
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarLeading) {
|
||||
if let onOpenSettings {
|
||||
Button("Open Settings") {
|
||||
Button {
|
||||
self.dismiss()
|
||||
onOpenSettings()
|
||||
} label: {
|
||||
Text("Open Settings")
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button("Done") {
|
||||
Button {
|
||||
self.dismiss()
|
||||
} label: {
|
||||
Text("Done")
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,9 +106,12 @@ struct GatewayProblemDetailsSheet: View {
|
||||
Text(verbatim: requestId)
|
||||
.font(OpenClawType.mono)
|
||||
.textSelection(.enabled)
|
||||
Button("Copy request ID") {
|
||||
Button {
|
||||
UIPasteboard.general.string = requestId
|
||||
self.copyFeedback = "Copied request ID"
|
||||
} label: {
|
||||
Text("Copy request ID")
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -118,9 +121,12 @@ struct GatewayProblemDetailsSheet: View {
|
||||
Text(verbatim: actionCommand)
|
||||
.font(OpenClawType.mono)
|
||||
.textSelection(.enabled)
|
||||
Button("Copy command") {
|
||||
Button {
|
||||
UIPasteboard.general.string = actionCommand
|
||||
self.copyFeedback = "Copied command"
|
||||
} label: {
|
||||
Text("Copy command")
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,6 +135,7 @@ struct GatewayProblemDetailsSheet: View {
|
||||
Section("Help") {
|
||||
Link(destination: docsURL) {
|
||||
Label("Open docs", systemImage: "book")
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
Text(verbatim: docsURL.absoluteString)
|
||||
.font(OpenClawType.footnote)
|
||||
@@ -159,15 +166,21 @@ struct GatewayProblemDetailsSheet: View {
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarLeading) {
|
||||
if let primaryActionTitle, let onPrimaryAction {
|
||||
Button(primaryActionTitle) {
|
||||
Button {
|
||||
self.dismiss()
|
||||
onPrimaryAction()
|
||||
} label: {
|
||||
Text(primaryActionTitle)
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button("Done") {
|
||||
Button {
|
||||
self.dismiss()
|
||||
} label: {
|
||||
Text("Done")
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ struct GatewayQuickSetupSheet: View {
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.font(OpenClawType.body)
|
||||
.padding()
|
||||
.navigationTitle("Quick Setup")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
@@ -97,6 +98,7 @@ struct GatewayQuickSetupSheet: View {
|
||||
self.dismiss()
|
||||
} label: {
|
||||
Text("Close")
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,20 +121,23 @@ struct GatewayQuickSetupSheet: View {
|
||||
}
|
||||
|
||||
private func fullRowToggle(_ title: LocalizedStringKey, isOn: Binding<Bool>) -> some View {
|
||||
Toggle(title, isOn: isOn)
|
||||
.contentShape(Rectangle())
|
||||
.overlay {
|
||||
// Keep Toggle semantics for accessibility while making the full visual row tappable.
|
||||
Button {
|
||||
isOn.wrappedValue.toggle()
|
||||
} label: {
|
||||
Rectangle()
|
||||
.fill(.clear)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityHidden(true)
|
||||
Toggle(isOn: isOn) {
|
||||
Text(title)
|
||||
.font(OpenClawType.subhead)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.overlay {
|
||||
// Keep Toggle semantics for accessibility while making the full visual row tappable.
|
||||
Button {
|
||||
isOn.wrappedValue.toggle()
|
||||
} label: {
|
||||
Rectangle()
|
||||
.fill(.clear)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
|
||||
private func gatewayProblemPrimaryActionTitle(_ problem: GatewayConnectionProblem) -> String? {
|
||||
|
||||
@@ -14,7 +14,7 @@ struct PrivacyAccessSectionView: View {
|
||||
@Environment(\.scenePhase) private var scenePhase
|
||||
|
||||
var body: some View {
|
||||
DisclosureGroup("Privacy & Access") {
|
||||
DisclosureGroup {
|
||||
self.permissionRow(
|
||||
title: "Contacts",
|
||||
icon: "person.crop.circle",
|
||||
@@ -54,7 +54,11 @@ struct PrivacyAccessSectionView: View {
|
||||
detail: "List, add, and complete reminders.",
|
||||
actionTitle: self.remindersActionTitle,
|
||||
action: self.handleRemindersAction)
|
||||
} label: {
|
||||
Text("Privacy & Access")
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
.font(OpenClawType.body)
|
||||
.onAppear { self.refreshAll() }
|
||||
.onChange(of: self.scenePhase) { _, phase in
|
||||
if phase == .active {
|
||||
@@ -74,6 +78,7 @@ struct PrivacyAccessSectionView: View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
HStack {
|
||||
Label(title, systemImage: icon)
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
Spacer()
|
||||
OpenClawStatusBadge(label: status, tone: self.statusTone(for: status))
|
||||
.accessibilityIdentifier("privacy-access-\(title)-status")
|
||||
@@ -82,10 +87,12 @@ struct PrivacyAccessSectionView: View {
|
||||
.font(OpenClawType.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
if let actionTitle, let action {
|
||||
Button(actionTitle, action: action)
|
||||
.font(OpenClawType.footnote)
|
||||
.buttonStyle(.bordered)
|
||||
.accessibilityIdentifier("privacy-access-\(title)-action")
|
||||
Button(action: action) {
|
||||
Text(actionTitle)
|
||||
.font(OpenClawType.footnoteSemiBold)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.accessibilityIdentifier("privacy-access-\(title)-action")
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
|
||||
@@ -89,6 +89,63 @@ struct OpenClawTypographyTests {
|
||||
#expect(!watchSource.contains(".font(.title"))
|
||||
}
|
||||
|
||||
@Test func `listed iOS app surfaces enforce branded control typography`() throws {
|
||||
let proComponents = try String(
|
||||
contentsOf: Self.sourceURL("Design/OpenClawProComponents.swift"),
|
||||
encoding: .utf8)
|
||||
let quickSetup = try String(contentsOf: Self.sourceURL("Gateway/GatewayQuickSetupSheet.swift"), encoding: .utf8)
|
||||
let settingsSections = try String(
|
||||
contentsOf: Self.sourceURL("Design/SettingsProTabSections.swift"),
|
||||
encoding: .utf8)
|
||||
let privacyAccess = try String(
|
||||
contentsOf: Self.sourceURL("Settings/PrivacyAccessSectionView.swift"),
|
||||
encoding: .utf8)
|
||||
let skillWorkshop = try String(
|
||||
contentsOf: Self.sourceURL("Design/IPadSkillWorkshopScreen.swift"),
|
||||
encoding: .utf8)
|
||||
let agentDestinations = try String(
|
||||
contentsOf: Self.sourceURL("Design/AgentProTab+Destinations.swift"),
|
||||
encoding: .utf8)
|
||||
let dreaming = try String(
|
||||
contentsOf: Self.sourceURL("Design/AgentProDreamingDestination.swift"),
|
||||
encoding: .utf8)
|
||||
let instances = try String(contentsOf: Self.sourceURL("Design/AgentProNodesDestination.swift"), encoding: .utf8)
|
||||
let channels = try String(
|
||||
contentsOf: Self.sourceURL("Design/SettingsChannelsDestination.swift"),
|
||||
encoding: .utf8)
|
||||
let docs = try String(contentsOf: Self.sourceURL("Design/OpenClawDocsScreen.swift"), encoding: .utf8)
|
||||
|
||||
#expect(proComponents.contains(".font(OpenClawType.subheadSemiBold)"))
|
||||
#expect(proComponents.contains("Text(primaryActionTitle)"))
|
||||
#expect(proComponents.contains("Text(secondaryActionTitle)"))
|
||||
|
||||
#expect(!quickSetup.contains("Button(\"Close\")"))
|
||||
#expect(quickSetup.contains("Text(\"Close\")"))
|
||||
#expect(quickSetup.contains(".font(OpenClawType.subheadSemiBold)"))
|
||||
|
||||
#expect(settingsSections.contains(".font(OpenClawType.body)"))
|
||||
#expect(settingsSections.contains("Toggle(isOn: self.$talkButtonEnabled)"))
|
||||
#expect(settingsSections.contains("Text(\"Show Talk Control\")"))
|
||||
#expect(settingsSections.contains("TextField(\"Default Share Instruction\""))
|
||||
#expect(settingsSections.contains(".font(OpenClawType.subhead)"))
|
||||
|
||||
#expect(!privacyAccess.contains("DisclosureGroup(\"Privacy & Access\")"))
|
||||
#expect(privacyAccess.contains("Text(\"Privacy & Access\")"))
|
||||
#expect(privacyAccess.contains("Text(actionTitle)"))
|
||||
#expect(privacyAccess.contains(".font(OpenClawType.footnoteSemiBold)"))
|
||||
|
||||
#expect(!skillWorkshop.contains("Button(\"Done\")"))
|
||||
#expect(skillWorkshop.contains("Label(\"Refresh\", systemImage: \"arrow.clockwise\")"))
|
||||
#expect(skillWorkshop.contains("Text(\"Default agent\")"))
|
||||
#expect(skillWorkshop.contains("Text(\"Inspect\")"))
|
||||
#expect(skillWorkshop.contains("Text(\"Apply\")"))
|
||||
#expect(skillWorkshop.contains("Text(\"Reject\")"))
|
||||
|
||||
for source in [agentDestinations, dreaming, instances, channels, docs] {
|
||||
#expect(source.contains(".font(OpenClawType.body)"))
|
||||
}
|
||||
}
|
||||
|
||||
private static let bundledFontFiles = [
|
||||
"RedHatDisplay[wght].ttf",
|
||||
"Inter[opsz,wght].ttf",
|
||||
@@ -118,6 +175,10 @@ struct OpenClawTypographyTests {
|
||||
self.iosRootURL().appendingPathComponent("WatchApp/Sources/WatchInboxView.swift")
|
||||
}
|
||||
|
||||
private static func sourceURL(_ relativePath: String) -> URL {
|
||||
self.iosRootURL().appendingPathComponent("Sources/\(relativePath)")
|
||||
}
|
||||
|
||||
private static func iosRootURL() -> URL {
|
||||
URL(fileURLWithPath: #filePath)
|
||||
.deletingLastPathComponent()
|
||||
|
||||
Reference in New Issue
Block a user