Files
openclaw/apps/ios/Sources/Design/AgentProTab+Destinations.swift
Peter Steinberger 0fff74c800 feat(ios): replace the tab bar with a black overlay sidebar (web parity + dashboard) (#111339)
* feat(ios): unify navigation on a black overlay sidebar and drop the phone tab bar

* test(ios): rework navigation tests and snapshot UITests for the sidebar shell

* chore(i18n): sync derived native catalogs for sidebar strings

* feat(ios): reveal the sidebar beneath a push-away rounded content card

* feat(ios): web-parity sidebar — agent switcher, pinned pages, sessions vocabulary, uniform content card

* chore(i18n): translate sidebar and session strings across native catalogs

* fix(ios): sidebar footer shows connection dot only when degraded, like the web

* feat(ios): sidebar toggle top-left with edge-swipe open; card surface owns the drawer chrome

* fix(ios): single sidebar refresh owner, alias-aware Home badges, RM tap-close, user pin order, drop vestigial idiom shims

* feat(ios): align sidebar with the mobile prototype — agent roster, theme-following palette, web session subtitles, row badges

* chore(i18n): translate agent roster and Recent strings across native catalogs

* feat(ios): frosted-glass agent picker with uniform drawer insets

* fix(ios): address sidebar review findings — settings toggle edge, per-agent scoping, alias-aware selection, cron paging, refresh cadence

* fix(ios): clear landing CI blockers
2026-07-19 14:50:39 -07:00

226 lines
7.7 KiB
Swift

import OpenClawKit
import OpenClawProtocol
import SwiftUI
extension AgentProTab {
@ViewBuilder
func destination(for route: AgentRoute) -> some View {
switch route {
case .agents:
self.agentsDestination
case .skills:
self.skillsDestination
case .instances:
self.instancesDestination
case .cron:
self.cronDestination
case .usage:
self.usageDestination
case .dreaming:
self.dreamingDestination
case .files:
self.filesDestination
}
}
var filesDestination: some View {
AgentWorkspaceFilesScreen(
agentId: self.activeAgentID,
headerSidebarAction: self.directHeaderSidebarAction(for: .files))
}
var agentsDestination: some View {
List {
Section {
if self.filteredAgents.isEmpty {
self.emptyAgentsRow
} else {
ForEach(self.filteredAgents, id: \.id) { agent in
self.agentRow(agent)
}
}
}
}
.listStyle(.insetGrouped)
.navigationTitle(self.headerTitle)
.navigationBarTitleDisplayMode(.large)
.searchable(text: self.$agentSearchText, prompt: "Search agents")
.refreshable {
await self.refreshOverview(force: true)
}
.font(OpenClawType.body)
.toolbar {
ToolbarItemGroup(placement: .topBarTrailing) {
self.agentFilterMenu
self.gatewayToolbarButton
}
if let headerSidebarAction {
ToolbarItem(placement: .topBarLeading) {
OpenClawSidebarHeaderLeadingSlot(action: headerSidebarAction)
}
}
}
}
var skillsDestination: some View {
ZStack {
OpenClawProBackground()
ScrollView {
VStack(alignment: .leading, spacing: 16) {
self.detailSummaryCard(
icon: "sparkles",
title: "Skills",
value: self.skillsValue,
detail: self.skillsDetail,
color: self.gatewayConnected ? OpenClawBrand.accent : .secondary)
self.skillsPolicyControls
self.skillsFilterField
self.clawHubSearchCard
self.skillsList
}
.padding(.vertical, 18)
.font(OpenClawType.body)
}
.refreshable {
await self.refreshOverview(force: true)
}
.safeAreaPadding(.bottom, OpenClawProMetric.bottomScrollInset)
}
.navigationTitle("Skills")
.navigationBarTitleDisplayMode(.inline)
}
var instancesDestination: some View {
AgentProNodesDestination(
headerSidebarAction: self.directHeaderSidebarAction(for: .instances),
overview: self.overview,
gatewayConnected: self.gatewayConnected,
agentCount: self.appModel.gatewayAgents.count,
instancesValue: self.instancesValue,
instancesDetail: self.instancesDetail,
instancesColor: self.instancesColor,
refresh: {
await self.refreshOverview(force: true)
})
}
var cronDestination: some View {
ZStack {
OpenClawProBackground()
ScrollView {
VStack(alignment: .leading, spacing: 16) {
self.directHeader(
for: .cron,
title: "Automations",
subtitle: self.cronDetail)
self.detailSummaryCard(
icon: "clock.arrow.circlepath",
title: "Automations",
value: self.cronValue,
detail: self.cronDetail,
color: self.cronColor)
self.cronStatusCard
self.cronJobsList(limit: nil)
}
.padding(.vertical, 18)
.font(OpenClawType.body)
}
.refreshable {
await self.refreshOverview(force: true)
}
.safeAreaPadding(.bottom, OpenClawProMetric.bottomScrollInset)
}
.navigationTitle("Automations")
.navigationBarTitleDisplayMode(.inline)
}
var usageDestination: some View {
ZStack {
OpenClawProBackground()
ScrollView {
VStack(alignment: .leading, spacing: 16) {
self.directHeader(
for: .usage,
title: "Usage",
subtitle: self.usageDetail)
self.detailSummaryCard(
icon: "chart.line.uptrend.xyaxis",
title: "Usage",
value: self.usageValue,
detail: self.usageDetail,
color: self.gatewayConnected ? OpenClawBrand.accent : .secondary)
self.usageTotalsCard
self.usageDailyList
}
.padding(.vertical, 18)
.font(OpenClawType.body)
}
.refreshable {
await self.refreshOverview(force: true)
}
.safeAreaPadding(.bottom, OpenClawProMetric.bottomScrollInset)
}
.navigationTitle("Usage")
.navigationBarTitleDisplayMode(.inline)
}
var dreamingDestination: some View {
AgentProDreamingDestination(
headerSidebarAction: self.directHeaderSidebarAction(for: .dreaming),
overview: self.overview,
gatewayConnected: self.gatewayConnected,
overviewLoading: self.overviewLoading,
dreamingValue: self.dreamingValue,
dreamingDetail: self.dreamingDetail,
dreamingColor: self.dreamingColor,
refresh: {
await self.refreshOverview(force: true)
})
}
@ViewBuilder
func directHeader(for route: AgentRoute, title: String, subtitle: String) -> some View {
if let headerSidebarAction = self.directHeaderSidebarAction(for: route) {
OpenClawAdaptiveHeaderRow(
title: .localized(title),
subtitle: .localized(subtitle),
titleFont: OpenClawType.title3SemiBold,
subtitleFont: OpenClawType.subheadMedium)
{
OpenClawSidebarHeaderLeadingSlot(action: headerSidebarAction)
} accessory: {
EmptyView()
}
.padding(.horizontal, OpenClawProMetric.pagePadding)
}
}
func directHeaderSidebarAction(for route: AgentRoute) -> OpenClawSidebarHeaderAction? {
self.directRoute == route ? self.headerSidebarAction : nil
}
func detailSummaryCard(
icon: String,
title: String,
value: String,
detail: String,
color: Color) -> some View
{
ProCard(radius: AgentLayout.cardRadius) {
HStack(spacing: 12) {
ProIconBadge(systemName: icon, color: color)
VStack(alignment: .leading, spacing: 3) {
Text(title)
.font(OpenClawType.headline)
Text(detail)
.font(OpenClawType.caption)
.foregroundStyle(.secondary)
}
Spacer(minLength: 8)
ProValuePill(value: value, color: color)
}
}
.padding(.horizontal, OpenClawProMetric.pagePadding)
}
}