mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 04:47:03 -06:00
feat(ios): implement branded typography design system
Add OpenClaw design system foundations: - Custom typography tokens (OpenClawType) using Plus Jakarta Sans, DM Sans, and JetBrains Mono with Dynamic Type scaling - Spacing and radius token enums (OpenClawSpacing, OpenClawRadius) - Additive brand color tokens (accentPressed, teal, void, obsidian, slate, textPrimary, textSecondary) - Shared components: OpenClawToggleIndicator, OpenClawStatusBadge, OpenClawPrimaryButtonStyle, OpenClawSecondaryButtonStyle - Bundled font files with OFL license provenance Migrate all iOS views to OpenClawType tokens. No behavioral, navigation, or routing changes — this is purely visual/typographic. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -122,9 +122,9 @@ struct AgentProDreamingDestination: View {
|
||||
ProIconBadge(systemName: icon, color: color)
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(title)
|
||||
.font(.headline)
|
||||
.font(OpenClawType.headline)
|
||||
Text(detail)
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer(minLength: 8)
|
||||
@@ -139,7 +139,7 @@ struct AgentProDreamingDestination: View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack {
|
||||
Text("Memory State")
|
||||
.font(.headline)
|
||||
.font(OpenClawType.headline)
|
||||
Spacer()
|
||||
ProValuePill(value: self.dreamingValue, color: self.dreamingColor)
|
||||
}
|
||||
@@ -156,7 +156,7 @@ struct AgentProDreamingDestination: View {
|
||||
}
|
||||
if let storeError = self.normalized(self.overview?.dreaming?.storeError) {
|
||||
Text(storeError)
|
||||
.font(.caption2)
|
||||
.font(OpenClawType.caption2)
|
||||
.foregroundStyle(OpenClawBrand.warn)
|
||||
}
|
||||
}
|
||||
@@ -170,9 +170,9 @@ struct AgentProDreamingDestination: View {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text("Maintenance")
|
||||
.font(.headline)
|
||||
.font(OpenClawType.headline)
|
||||
Text("Refresh reads live state. Maintenance actions update the gateway diary/artifacts.")
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
}
|
||||
@@ -203,7 +203,7 @@ struct AgentProDreamingDestination: View {
|
||||
|
||||
if let dreamActionStatusText {
|
||||
Text(dreamActionStatusText)
|
||||
.font(.caption2)
|
||||
.font(OpenClawType.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(3)
|
||||
}
|
||||
@@ -225,10 +225,10 @@ struct AgentProDreamingDestination: View {
|
||||
ProIconBadge(systemName: "book.pages", color: OpenClawBrand.accent)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(diary.path)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.lineLimit(1)
|
||||
Text(self.dreamDiaryUpdatedLabel(diary))
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer(minLength: 8)
|
||||
@@ -287,7 +287,7 @@ struct AgentProDreamingDestination: View {
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.75)
|
||||
}
|
||||
.font(.caption.weight(.semibold))
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.foregroundStyle(.primary)
|
||||
.padding(.horizontal, 10)
|
||||
.frame(height: 34)
|
||||
@@ -300,22 +300,24 @@ struct AgentProDreamingDestination: View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack(alignment: .firstTextBaseline) {
|
||||
Text(day.title)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.lineLimit(1)
|
||||
Spacer(minLength: 8)
|
||||
Text("\(day.entryCount) \(day.entryCount == 1 ? "entry" : "entries")")
|
||||
.font(.caption2.weight(.semibold))
|
||||
.font(OpenClawType.caption2SemiBold)
|
||||
.foregroundStyle(OpenClawBrand.accent)
|
||||
}
|
||||
Text(day.body)
|
||||
.font(.caption.monospaced())
|
||||
.font(OpenClawType.monoSmall)
|
||||
.foregroundStyle(.primary)
|
||||
.lineLimit(120)
|
||||
.textSelection(.enabled)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
.padding(10)
|
||||
.background(Color.primary.opacity(0.045), in: RoundedRectangle(cornerRadius: 10, style: .continuous))
|
||||
.background(
|
||||
Color.primary.opacity(0.045),
|
||||
in: RoundedRectangle(cornerRadius: OpenClawRadius.sm, style: .continuous))
|
||||
}
|
||||
|
||||
private func selectedDreamDiaryDay(from days: [DreamDiaryDay]) -> DreamDiaryDay? {
|
||||
@@ -362,21 +364,21 @@ struct AgentProDreamingDestination: View {
|
||||
ProIconBadge(systemName: "text.page", color: OpenClawBrand.accent)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(self.dreamingEntryTitle(entry))
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.lineLimit(1)
|
||||
Text(entry.snippet)
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(4)
|
||||
.textSelection(.enabled)
|
||||
Text(self.dreamingEntryDetail(entry))
|
||||
.font(.caption2)
|
||||
.font(OpenClawType.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
Spacer(minLength: 8)
|
||||
Text("\(entry.totalSignalCount)")
|
||||
.font(.caption2.weight(.semibold))
|
||||
.font(OpenClawType.caption2SemiBold)
|
||||
.foregroundStyle(OpenClawBrand.accent)
|
||||
.lineLimit(1)
|
||||
}
|
||||
@@ -428,21 +430,21 @@ struct AgentProDreamingDestination: View {
|
||||
color: phase.status.enabled == false ? .secondary : OpenClawBrand.accent)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(phase.title)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
Text(self.dreamingPhaseDetail(phase.status))
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
if let cron = self.normalized(phase.status.cron) {
|
||||
Text(cron)
|
||||
.font(.caption2)
|
||||
.font(OpenClawType.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
Spacer(minLength: 8)
|
||||
Text(self.dreamingPhaseState(phase.status))
|
||||
.font(.caption2.weight(.semibold))
|
||||
.font(OpenClawType.caption2SemiBold)
|
||||
.foregroundStyle(phase.status.managedCronPresent == true ? OpenClawBrand.accent : .secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
@@ -455,9 +457,9 @@ struct AgentProDreamingDestination: View {
|
||||
ProIconBadge(systemName: icon, color: .secondary)
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(title)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
Text(detail)
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
}
|
||||
@@ -468,16 +470,18 @@ struct AgentProDreamingDestination: View {
|
||||
private func detailMetric(label: String, value: String) -> some View {
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(label)
|
||||
.font(.caption2.weight(.medium))
|
||||
.font(OpenClawType.caption2Medium)
|
||||
.foregroundStyle(.secondary)
|
||||
Text(value)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.8)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(10)
|
||||
.background(Color.primary.opacity(0.055), in: RoundedRectangle(cornerRadius: 10, style: .continuous))
|
||||
.background(
|
||||
Color.primary.opacity(0.055),
|
||||
in: RoundedRectangle(cornerRadius: OpenClawRadius.sm, style: .continuous))
|
||||
}
|
||||
|
||||
private func dreamingEntryTitle(_ entry: DreamingEntryLite) -> String {
|
||||
|
||||
@@ -56,9 +56,9 @@ struct AgentProNodesDestination: View {
|
||||
ProIconBadge(systemName: "display", color: self.instancesColor)
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text("Instances")
|
||||
.font(.headline)
|
||||
.font(OpenClawType.headline)
|
||||
Text(self.instancesDetail)
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer(minLength: 8)
|
||||
@@ -73,7 +73,7 @@ struct AgentProNodesDestination: View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack {
|
||||
Text("Presence")
|
||||
.font(.headline)
|
||||
.font(OpenClawType.headline)
|
||||
Spacer()
|
||||
ProValuePill(value: self.instancesValue, color: self.instancesColor)
|
||||
}
|
||||
@@ -134,27 +134,27 @@ struct AgentProNodesDestination: View {
|
||||
ProIconBadge(systemName: Self.presenceIcon(entry), color: Self.presenceColor(entry))
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(Self.presenceLabel(entry) ?? "Instance")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.lineLimit(1)
|
||||
Text(Self.presenceDetail(entry))
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
if let meta = Self.presenceMeta(entry) {
|
||||
Text(meta)
|
||||
.font(.caption2)
|
||||
.font(OpenClawType.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
Spacer(minLength: 8)
|
||||
Text(Self.presenceState(entry))
|
||||
.font(.caption2.weight(.semibold))
|
||||
.font(OpenClawType.caption2SemiBold)
|
||||
.foregroundStyle(Self.presenceColor(entry))
|
||||
.lineLimit(1)
|
||||
if showsChevron {
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.caption2.weight(.bold))
|
||||
.font(OpenClawType.caption2Bold)
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.top, 2)
|
||||
}
|
||||
@@ -173,9 +173,9 @@ struct AgentProNodesDestination: View {
|
||||
ProIconBadge(systemName: Self.presenceIcon(entry), color: Self.presenceColor(entry))
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(Self.presenceLabel(entry) ?? "Instance")
|
||||
.font(.headline)
|
||||
.font(OpenClawType.headline)
|
||||
Text(Self.presenceDetail(entry))
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer(minLength: 8)
|
||||
@@ -233,7 +233,7 @@ struct AgentProNodesDestination: View {
|
||||
.disabled(normalized == "n/a")
|
||||
.accessibilityLabel("Copy \(title)")
|
||||
}
|
||||
.font(.subheadline)
|
||||
.font(OpenClawType.subhead)
|
||||
.padding(.vertical, 10)
|
||||
}
|
||||
|
||||
@@ -243,14 +243,14 @@ struct AgentProNodesDestination: View {
|
||||
ProCard {
|
||||
if values.isEmpty {
|
||||
Text("None reported.")
|
||||
.font(.subheadline)
|
||||
.font(OpenClawType.subhead)
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
} else {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
ForEach(values, id: \.self) { value in
|
||||
Text(value)
|
||||
.font(.caption.monospaced())
|
||||
.font(OpenClawType.monoSmall)
|
||||
.textSelection(.enabled)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
@@ -264,16 +264,18 @@ struct AgentProNodesDestination: View {
|
||||
private func detailMetric(label: String, value: String) -> some View {
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(label)
|
||||
.font(.caption2.weight(.medium))
|
||||
.font(OpenClawType.caption2Medium)
|
||||
.foregroundStyle(.secondary)
|
||||
Text(value)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.8)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(10)
|
||||
.background(Color.primary.opacity(0.055), in: RoundedRectangle(cornerRadius: 10, style: .continuous))
|
||||
.background(
|
||||
Color.primary.opacity(0.055),
|
||||
in: RoundedRectangle(cornerRadius: OpenClawRadius.sm, style: .continuous))
|
||||
}
|
||||
|
||||
private func emptyRow(icon: String, title: String, detail: String) -> some View {
|
||||
@@ -281,9 +283,9 @@ struct AgentProNodesDestination: View {
|
||||
ProIconBadge(systemName: icon, color: .secondary)
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(title)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
Text(detail)
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ extension AgentProTab {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack {
|
||||
Text("Scheduler")
|
||||
.font(.headline)
|
||||
.font(OpenClawType.headline)
|
||||
Spacer()
|
||||
ProValuePill(
|
||||
value: self.overview?.cronStatus?.enabled == true ? "on" : "off",
|
||||
@@ -23,7 +23,7 @@ extension AgentProTab {
|
||||
}
|
||||
if let cronActionStatusText {
|
||||
Text(cronActionStatusText)
|
||||
.font(.caption2)
|
||||
.font(OpenClawType.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
}
|
||||
@@ -83,14 +83,14 @@ extension AgentProTab {
|
||||
color: job.enabled ? OpenClawBrand.accent : .secondary)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(job.name)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.lineLimit(1)
|
||||
Text(self.cronJobDetail(job))
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
Text(self.cronScheduleSummary(job))
|
||||
.font(.caption2)
|
||||
.font(OpenClawType.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
HStack(spacing: 8) {
|
||||
@@ -118,7 +118,7 @@ extension AgentProTab {
|
||||
.controlSize(.small)
|
||||
} else {
|
||||
Text(self.cronJobState(job))
|
||||
.font(.caption2.weight(.semibold))
|
||||
.font(OpenClawType.caption2SemiBold)
|
||||
.foregroundStyle(job.enabled ? OpenClawBrand.accent : .secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
|
||||
@@ -199,9 +199,9 @@ extension AgentProTab {
|
||||
ProIconBadge(systemName: icon, color: color)
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(title)
|
||||
.font(.headline)
|
||||
.font(OpenClawType.headline)
|
||||
Text(detail)
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer(minLength: 8)
|
||||
|
||||
@@ -5,16 +5,18 @@ extension AgentProTab {
|
||||
func detailMetric(label: String, value: String) -> some View {
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(label)
|
||||
.font(.caption2.weight(.medium))
|
||||
.font(OpenClawType.caption2Medium)
|
||||
.foregroundStyle(.secondary)
|
||||
Text(value)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.8)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(10)
|
||||
.background(Color.primary.opacity(0.055), in: RoundedRectangle(cornerRadius: 10, style: .continuous))
|
||||
.background(
|
||||
Color.primary.opacity(0.055),
|
||||
in: RoundedRectangle(cornerRadius: OpenClawRadius.sm, style: .continuous))
|
||||
}
|
||||
|
||||
func emptyDetailRow(icon: String, title: String, detail: String) -> some View {
|
||||
@@ -22,9 +24,9 @@ extension AgentProTab {
|
||||
ProIconBadge(systemName: icon, color: .secondary)
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(title)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
Text(detail)
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ extension AgentProTab {
|
||||
TextField("Search agents", text: self.$agentSearchText)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
.font(.subheadline)
|
||||
.font(OpenClawType.subhead)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.frame(height: 38)
|
||||
.transition(.move(edge: .top).combined(with: .opacity))
|
||||
@@ -77,7 +77,7 @@ extension AgentProTab {
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.font(.title3)
|
||||
.font(OpenClawType.title3)
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(width: 44, height: 44)
|
||||
.contentShape(Circle())
|
||||
@@ -185,7 +185,7 @@ extension AgentProTab {
|
||||
|
||||
if let overviewErrorText {
|
||||
Text(overviewErrorText)
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(OpenClawBrand.warn)
|
||||
.padding(.horizontal, OpenClawProMetric.pagePadding)
|
||||
}
|
||||
@@ -245,9 +245,9 @@ extension AgentProTab {
|
||||
ProIconBadge(systemName: "person.2.slash", color: .secondary)
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(self.emptyAgentsTitle)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
Text(self.emptyAgentsDetail)
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
@@ -266,12 +266,12 @@ extension AgentProTab {
|
||||
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(self.agentName(for: agent))
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.foregroundStyle(.primary)
|
||||
.lineLimit(1)
|
||||
|
||||
Text(self.agentDetail(for: agent))
|
||||
.font(.footnote)
|
||||
.font(OpenClawType.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
@@ -281,7 +281,7 @@ extension AgentProTab {
|
||||
|
||||
if isActive {
|
||||
Image(systemName: "checkmark")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.foregroundStyle(OpenClawBrand.accent)
|
||||
.frame(width: 24, height: 44)
|
||||
.accessibilityHidden(true)
|
||||
@@ -302,7 +302,7 @@ extension AgentProTab {
|
||||
{
|
||||
Button(action: action) {
|
||||
Image(systemName: systemName)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.frame(width: AgentLayout.filterHeight, height: AgentLayout.filterHeight)
|
||||
}
|
||||
.buttonBorderShape(.circle)
|
||||
@@ -342,20 +342,20 @@ extension AgentProTab {
|
||||
ProIconBadge(systemName: icon, color: color)
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(title)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
Text(detail)
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
Spacer(minLength: 8)
|
||||
Text(value)
|
||||
.font(.caption2.weight(.semibold))
|
||||
.font(OpenClawType.caption2SemiBold)
|
||||
.foregroundStyle(color)
|
||||
.lineLimit(1)
|
||||
if showsChevron {
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.caption.weight(.semibold))
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
@@ -410,15 +410,15 @@ extension AgentProTab {
|
||||
ProValuePill(value: value, color: color)
|
||||
if showsChevron {
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.caption.weight(.semibold))
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(title)
|
||||
.font(.caption.weight(.semibold))
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
Text(detail)
|
||||
.font(.caption2)
|
||||
.font(OpenClawType.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
.multilineTextAlignment(.leading)
|
||||
@@ -434,11 +434,11 @@ extension AgentProTab {
|
||||
ProIconBadge(systemName: "clock.badge.questionmark", color: .secondary)
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(self.gatewayConnected ? "No scheduled jobs" : "Cron unavailable")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
Text(self.gatewayConnected
|
||||
? "The gateway has no visible cron jobs."
|
||||
: "Connect a gateway to load scheduled work.")
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
@@ -452,16 +452,16 @@ extension AgentProTab {
|
||||
color: job.enabled ? OpenClawBrand.accent : .secondary)
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(job.name)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.lineLimit(1)
|
||||
Text(self.cronJobDetail(job))
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
Spacer(minLength: 8)
|
||||
Text(self.cronJobState(job))
|
||||
.font(.caption2.weight(.semibold))
|
||||
.font(OpenClawType.caption2SemiBold)
|
||||
.foregroundStyle(job.enabled ? OpenClawBrand.accent : .secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
|
||||
@@ -9,9 +9,9 @@ extension AgentProTab {
|
||||
HStack(alignment: .firstTextBaseline) {
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(self.activeAgentName)
|
||||
.font(.headline)
|
||||
.font(OpenClawType.headline)
|
||||
Text(self.skillPolicySummary)
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer(minLength: 8)
|
||||
@@ -41,12 +41,12 @@ extension AgentProTab {
|
||||
|
||||
if let skillMutationStatusText {
|
||||
Text(skillMutationStatusText)
|
||||
.font(.caption2)
|
||||
.font(OpenClawType.caption2)
|
||||
.foregroundStyle(OpenClawBrand.accent)
|
||||
}
|
||||
if let skillMutationErrorText {
|
||||
Text(skillMutationErrorText)
|
||||
.font(.caption2)
|
||||
.font(OpenClawType.caption2)
|
||||
.foregroundStyle(OpenClawBrand.warn)
|
||||
}
|
||||
}
|
||||
@@ -59,12 +59,12 @@ extension AgentProTab {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
HStack(spacing: 10) {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.font(.caption.weight(.semibold))
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.foregroundStyle(.secondary)
|
||||
TextField("Search skills", text: self.$skillFilter)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
.font(.subheadline)
|
||||
.font(OpenClawType.subhead)
|
||||
if !self.skillFilter.isEmpty {
|
||||
Button {
|
||||
self.skillFilter = ""
|
||||
@@ -94,9 +94,9 @@ extension AgentProTab {
|
||||
ProIconBadge(systemName: "square.and.arrow.down", color: OpenClawBrand.accent)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Install Skills")
|
||||
.font(.headline)
|
||||
.font(OpenClawType.headline)
|
||||
Text("Search ClawHub and install into this workspace.")
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer(minLength: 8)
|
||||
@@ -114,7 +114,7 @@ extension AgentProTab {
|
||||
TextField("Search ClawHub", text: self.$clawHubQuery)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
.font(.subheadline)
|
||||
.font(OpenClawType.subhead)
|
||||
.submitLabel(.search)
|
||||
.onSubmit {
|
||||
Task { await self.searchClawHubSkills() }
|
||||
@@ -126,7 +126,7 @@ extension AgentProTab {
|
||||
}
|
||||
if let clawHubErrorText {
|
||||
Text(clawHubErrorText)
|
||||
.font(.caption2)
|
||||
.font(OpenClawType.caption2)
|
||||
.foregroundStyle(OpenClawBrand.warn)
|
||||
}
|
||||
if !self.clawHubResults.isEmpty {
|
||||
@@ -151,10 +151,10 @@ extension AgentProTab {
|
||||
ProIconBadge(systemName: "sparkles", color: OpenClawBrand.accent)
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(result.displayName)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.lineLimit(1)
|
||||
Text(result.summary ?? result.slug)
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
}
|
||||
@@ -277,21 +277,21 @@ extension AgentProTab {
|
||||
ProIconBadge(systemName: self.isSkillAllowed(skill) ? "checkmark.circle" : "nosign", color: status.color)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(skill.displayName)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.lineLimit(1)
|
||||
Text(self.normalized(skill.description) ?? self.normalized(skill.source) ?? "Workspace skill")
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
if let missing = skill.missingSummary {
|
||||
Text("Missing: \(missing)")
|
||||
.font(.caption2)
|
||||
.font(OpenClawType.caption2)
|
||||
.foregroundStyle(OpenClawBrand.warn)
|
||||
.lineLimit(1)
|
||||
}
|
||||
if let install = skill.installSummary {
|
||||
Text("Setup: \(install)")
|
||||
.font(.caption2)
|
||||
.font(OpenClawType.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
@@ -321,7 +321,7 @@ extension AgentProTab {
|
||||
.accessibilityLabel("Edit \(skill.displayName)")
|
||||
}
|
||||
Text(busy ? "saving" : status.text)
|
||||
.font(.caption2.weight(.semibold))
|
||||
.font(OpenClawType.caption2SemiBold)
|
||||
.foregroundStyle(status.color)
|
||||
.lineLimit(1)
|
||||
}
|
||||
@@ -424,9 +424,9 @@ extension AgentProTab {
|
||||
color: status.color)
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(skill.displayName)
|
||||
.font(.headline)
|
||||
.font(OpenClawType.headline)
|
||||
Text(self.normalized(skill.description) ?? self.normalized(skill.source) ?? "Workspace skill")
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(3)
|
||||
}
|
||||
@@ -451,7 +451,7 @@ extension AgentProTab {
|
||||
if let primaryEnv = skill.primaryEnv, !primaryEnv.isEmpty {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("API key")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
SecureField(primaryEnv, text: self.skillAPIKeyBinding(for: skill))
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
@@ -465,14 +465,14 @@ extension AgentProTab {
|
||||
.disabled(self.isSkillConfigBusy(skill))
|
||||
if let homepage = skill.homepageURL {
|
||||
Link("Get key", destination: homepage)
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let message = self.skillConfigMessages[skill.effectiveSkillKey] {
|
||||
Text(message.text)
|
||||
.font(.caption2)
|
||||
.font(OpenClawType.caption2)
|
||||
.foregroundStyle(message.kind == .success ? OpenClawBrand.accent : OpenClawBrand.warn)
|
||||
}
|
||||
}
|
||||
@@ -494,7 +494,7 @@ extension AgentProTab {
|
||||
HStack {
|
||||
Text(title)
|
||||
Spacer(minLength: 8)
|
||||
self.skillEditorSwitchIndicator(isOn: isOn)
|
||||
OpenClawToggleIndicator(isOn: isOn)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
@@ -504,31 +504,18 @@ extension AgentProTab {
|
||||
.accessibilityValue(isOn ? "On" : "Off")
|
||||
}
|
||||
|
||||
func skillEditorSwitchIndicator(isOn: Bool) -> some View {
|
||||
Capsule()
|
||||
.fill(isOn ? OpenClawBrand.accent : Color.secondary.opacity(0.35))
|
||||
.frame(width: 52, height: 32)
|
||||
.overlay(alignment: isOn ? .trailing : .leading) {
|
||||
Circle()
|
||||
.fill(Color.white)
|
||||
.frame(width: 28, height: 28)
|
||||
.padding(2)
|
||||
.shadow(color: Color.black.opacity(0.14), radius: 1, x: 0, y: 1)
|
||||
}
|
||||
}
|
||||
|
||||
func skillEditorSetup(_ skill: SkillStatusEntryLite) -> some View {
|
||||
ProCard(radius: AgentLayout.cardRadius) {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text("Setup")
|
||||
.font(.headline)
|
||||
.font(OpenClawType.headline)
|
||||
if let missing = skill.missingSummary {
|
||||
Text("Missing: \(missing)")
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(OpenClawBrand.warn)
|
||||
} else {
|
||||
Text("No missing requirements reported.")
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
if let install = skill.install?.first {
|
||||
@@ -553,7 +540,7 @@ extension AgentProTab {
|
||||
self.detailMetric(label: "Source", value: self.normalized(skill.source) ?? "unknown")
|
||||
if let filePath = self.normalized(skill.filePath) {
|
||||
Text(filePath)
|
||||
.font(.caption2.monospaced())
|
||||
.font(OpenClawType.monoSmall)
|
||||
.foregroundStyle(.secondary)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ extension AgentProTab {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack {
|
||||
Text("Totals")
|
||||
.font(.headline)
|
||||
.font(OpenClawType.headline)
|
||||
Spacer()
|
||||
ProValuePill(
|
||||
value: "\(self.overview?.usage?.days ?? 31)d",
|
||||
@@ -67,14 +67,14 @@ extension AgentProTab {
|
||||
ProIconBadge(systemName: "calendar", color: OpenClawBrand.accent)
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(day.date)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
Text("\(Self.compactNumber(day.totalTokens ?? 0)) tokens")
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer(minLength: 8)
|
||||
Text(Self.currency(day.totalCost ?? 0))
|
||||
.font(.caption2.weight(.semibold))
|
||||
.font(OpenClawType.caption2SemiBold)
|
||||
.foregroundStyle(OpenClawBrand.accent)
|
||||
}
|
||||
.padding(.vertical, 10)
|
||||
|
||||
@@ -79,7 +79,6 @@ struct ChatProTab: View {
|
||||
composerChrome: .clean,
|
||||
isComposerEnabled: self.gatewayConnected,
|
||||
messagePlaceholder: self.messagePlaceholder,
|
||||
emptyAssistantIntro: "What would you like to work on?",
|
||||
talkControl: self.talkControl)
|
||||
.id(ObjectIdentifier(viewModel))
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
@@ -87,9 +86,9 @@ struct ChatProTab: View {
|
||||
ProCard {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Chat is preparing")
|
||||
.font(.headline)
|
||||
.font(OpenClawType.headline)
|
||||
Text("The operator session will attach when the gateway is ready.")
|
||||
.font(.subheadline)
|
||||
.font(OpenClawType.subhead)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
@@ -108,8 +107,8 @@ struct ChatProTab: View {
|
||||
OpenClawAdaptiveHeaderRow(
|
||||
title: self.headerDisplayTitle,
|
||||
subtitle: self.headerDisplaySubtitle,
|
||||
titleFont: .headline.weight(.semibold),
|
||||
subtitleFont: .caption,
|
||||
titleFont: OpenClawType.headline,
|
||||
subtitleFont: OpenClawType.caption,
|
||||
subtitleLineLimit: 1)
|
||||
{
|
||||
HStack(spacing: 11) {
|
||||
@@ -119,25 +118,26 @@ struct ChatProTab: View {
|
||||
self.headerIdentityBadge
|
||||
}
|
||||
} accessory: {
|
||||
self.connectionStatusButton
|
||||
self.connectionPillButton
|
||||
}
|
||||
.padding(.horizontal, OpenClawProMetric.pagePadding)
|
||||
.padding(.bottom, 2)
|
||||
.padding(.bottom, 4)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var headerIdentityBadge: some View {
|
||||
if self.showsAgentBadge {
|
||||
Text(self.agentBadge)
|
||||
.font(.system(size: self.agentBadge.count > 2 ? 11 : 14, weight: .bold, design: .rounded))
|
||||
.font(.system(size: self.agentBadge.count > 2 ? 13 : 16, weight: .bold, design: .rounded))
|
||||
.foregroundStyle(.white)
|
||||
.minimumScaleFactor(0.6)
|
||||
.lineLimit(1)
|
||||
.frame(width: 32, height: 32)
|
||||
.frame(width: 38, height: 38)
|
||||
.background(
|
||||
Circle()
|
||||
.fill(OpenClawBrand.accent))
|
||||
.fill(OpenClawBrand.accent.gradient))
|
||||
.overlay(Circle().strokeBorder(.white.opacity(0.18), lineWidth: 1))
|
||||
.shadow(color: OpenClawBrand.accent.opacity(0.14), radius: 5, y: 2)
|
||||
} else {
|
||||
ProIconBadge(systemName: "bubble.left", color: OpenClawBrand.accent)
|
||||
}
|
||||
@@ -196,44 +196,29 @@ struct ChatProTab: View {
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var connectionStatusButton: some View {
|
||||
private var connectionPillButton: some View {
|
||||
if let openSettings {
|
||||
Button(action: openSettings) {
|
||||
self.connectionStatusIcon
|
||||
self.connectionPill
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.contentShape(Circle())
|
||||
.accessibilityLabel(self.gatewayAccessibilityLabel)
|
||||
.buttonBorderShape(.capsule)
|
||||
.openClawGlassButton()
|
||||
.accessibilityHint("Opens Settings / Gateway")
|
||||
.accessibilityIdentifier("chat-gateway-status")
|
||||
} else {
|
||||
self.connectionStatusIcon
|
||||
.accessibilityLabel(self.gatewayAccessibilityLabel)
|
||||
self.connectionPill
|
||||
}
|
||||
}
|
||||
|
||||
private var connectionStatusIcon: some View {
|
||||
Image(systemName: self.gatewayStatusSymbol)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundStyle(self.gatewayPillColor)
|
||||
.frame(width: 44, height: 44)
|
||||
}
|
||||
|
||||
private var gatewayStatusSymbol: String {
|
||||
switch self.gatewayDisplayState {
|
||||
case .connected:
|
||||
self.gatewayConnected ? "checkmark.circle.fill" : "exclamationmark.circle"
|
||||
case .connecting:
|
||||
"arrow.trianglehead.2.clockwise.rotate.90"
|
||||
case .error:
|
||||
"exclamationmark.triangle.fill"
|
||||
case .disconnected:
|
||||
"wifi.slash"
|
||||
private var connectionPill: some View {
|
||||
HStack(spacing: 6) {
|
||||
ProStatusDot(color: self.gatewayPillColor)
|
||||
Text(Self.gatewayPillTitle(state: self.gatewayDisplayState, isGatewayUsable: self.gatewayConnected))
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
|
||||
private var gatewayAccessibilityLabel: String {
|
||||
"Gateway: \(Self.gatewayPillTitle(state: self.gatewayDisplayState, isGatewayUsable: self.gatewayConnected))"
|
||||
.foregroundStyle(self.gatewayPillColor)
|
||||
.padding(.horizontal, 4)
|
||||
.frame(height: 30)
|
||||
}
|
||||
|
||||
private var gatewayConnected: Bool {
|
||||
|
||||
@@ -42,27 +42,27 @@ struct CommandSessionRow: View {
|
||||
var body: some View {
|
||||
HStack(alignment: .center, spacing: 12) {
|
||||
Image(systemName: self.item.icon)
|
||||
.font(.caption.weight(.semibold))
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.foregroundStyle(self.item.color)
|
||||
.frame(width: 30, height: 30)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 9, style: .continuous)
|
||||
RoundedRectangle(cornerRadius: OpenClawRadius.sm, style: .continuous)
|
||||
.fill(self.item.color.opacity(0.12))
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
||||
Text(self.item.title)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.82)
|
||||
Spacer(minLength: 6)
|
||||
Text(self.item.trailing)
|
||||
.font(.caption2.weight(.medium))
|
||||
.font(OpenClawType.caption2Medium)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
HStack(spacing: 8) {
|
||||
Text(self.item.detail)
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
Spacer(minLength: 6)
|
||||
@@ -71,7 +71,7 @@ struct CommandSessionRow: View {
|
||||
.frame(width: 68)
|
||||
}
|
||||
Text(self.progressLabel)
|
||||
.font(.caption.weight(.semibold))
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.foregroundStyle(self.item.color)
|
||||
.lineLimit(1)
|
||||
.frame(width: 48, alignment: .trailing)
|
||||
@@ -97,7 +97,7 @@ struct CommandSessionRow: View {
|
||||
struct CommandViewMoreRow: View {
|
||||
var body: some View {
|
||||
Label("View More", systemImage: "chevron.right")
|
||||
.font(.subheadline.weight(.bold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.foregroundStyle(OpenClawBrand.accent)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 10)
|
||||
@@ -113,19 +113,19 @@ struct CommandEmptyStateRow: View {
|
||||
var body: some View {
|
||||
HStack(spacing: 10) {
|
||||
Image(systemName: self.icon)
|
||||
.font(.caption.weight(.bold))
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.foregroundStyle(OpenClawBrand.ok)
|
||||
.frame(width: 30, height: 30)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 8, style: .continuous)
|
||||
RoundedRectangle(cornerRadius: OpenClawRadius.xs, style: .continuous)
|
||||
.fill(OpenClawBrand.ok.opacity(0.10))
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(self.title)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.lineLimit(1)
|
||||
Text(self.detail)
|
||||
.font(.caption2.weight(.medium))
|
||||
.font(OpenClawType.caption2Medium)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ struct CommandCenterTab: View {
|
||||
} accessory: {
|
||||
Button(action: self.openSettings) {
|
||||
Image(systemName: "gearshape.fill")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.frame(width: OpenClawProMetric.compactControlSize, height: OpenClawProMetric.compactControlSize)
|
||||
}
|
||||
.openClawGlassButton()
|
||||
@@ -195,15 +195,15 @@ struct CommandCenterTab: View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
HStack(spacing: 5) {
|
||||
Image(systemName: icon)
|
||||
.font(.caption2.weight(.bold))
|
||||
.font(OpenClawType.caption2Bold)
|
||||
.foregroundStyle(color)
|
||||
Text(title)
|
||||
.font(.caption2.weight(.medium))
|
||||
.font(OpenClawType.caption2Medium)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
Text(value)
|
||||
.font(.caption.weight(.semibold))
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.foregroundStyle(title == "Connection" ? color : .primary)
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.72)
|
||||
@@ -275,7 +275,7 @@ struct CommandCenterTab: View {
|
||||
private func cardHeader(title: String) -> some View {
|
||||
HStack(spacing: 8) {
|
||||
Text(title)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.foregroundStyle(.secondary)
|
||||
Spacer(minLength: 8)
|
||||
}
|
||||
@@ -659,9 +659,9 @@ struct CommandSessionsScreen: View {
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Sessions")
|
||||
.font(.system(size: 27, weight: .bold, design: .rounded))
|
||||
.font(OpenClawType.title2)
|
||||
Text(self.headerDetail)
|
||||
.font(.caption.weight(.medium))
|
||||
.font(OpenClawType.captionMedium)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
@@ -673,7 +673,7 @@ struct CommandSessionsScreen: View {
|
||||
VStack(spacing: 0) {
|
||||
HStack(spacing: 8) {
|
||||
Text("Recent sessions")
|
||||
.font(.subheadline.weight(.bold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
Spacer(minLength: 8)
|
||||
if self.isLoading {
|
||||
ProgressView()
|
||||
|
||||
@@ -64,7 +64,7 @@ struct IPadSkillWorkshopScreen: View {
|
||||
ScrollView {
|
||||
self.presentedProposalDetail(proposalID: route.proposalID)
|
||||
.padding(.horizontal, OpenClawProMetric.pagePadding)
|
||||
.padding(.vertical, 16)
|
||||
.padding(.vertical, OpenClawSpacing.space4)
|
||||
}
|
||||
.background(OpenClawProBackground())
|
||||
.navigationTitle("Proposal")
|
||||
@@ -130,12 +130,12 @@ struct IPadSkillWorkshopScreen: View {
|
||||
}
|
||||
if let noticeText {
|
||||
Text(noticeText)
|
||||
.font(.caption2)
|
||||
.font(OpenClawType.caption2)
|
||||
.foregroundStyle(OpenClawBrand.accent)
|
||||
}
|
||||
if let errorText {
|
||||
Text(errorText)
|
||||
.font(.caption2)
|
||||
.font(OpenClawType.caption2)
|
||||
.foregroundStyle(OpenClawBrand.warn)
|
||||
}
|
||||
}
|
||||
@@ -149,9 +149,9 @@ struct IPadSkillWorkshopScreen: View {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 10) {
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text("\(self.filteredProposals.count) proposals")
|
||||
.font(.headline)
|
||||
.font(OpenClawType.headline)
|
||||
Text(self.statusFilterLabel)
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer(minLength: 8)
|
||||
@@ -186,12 +186,12 @@ struct IPadSkillWorkshopScreen: View {
|
||||
}
|
||||
if let noticeText {
|
||||
Text(noticeText)
|
||||
.font(.caption2)
|
||||
.font(OpenClawType.caption2)
|
||||
.foregroundStyle(OpenClawBrand.accent)
|
||||
}
|
||||
if let errorText {
|
||||
Text(errorText)
|
||||
.font(.caption2)
|
||||
.font(OpenClawType.caption2)
|
||||
.foregroundStyle(OpenClawBrand.warn)
|
||||
}
|
||||
}
|
||||
@@ -202,12 +202,12 @@ struct IPadSkillWorkshopScreen: View {
|
||||
private var proposalSearchField: some View {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.font(.caption.weight(.semibold))
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.foregroundStyle(.secondary)
|
||||
TextField("Search proposals", text: self.$query)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
.font(.subheadline)
|
||||
.font(OpenClawType.subhead)
|
||||
if !self.query.isEmpty {
|
||||
Button {
|
||||
self.query = ""
|
||||
@@ -223,7 +223,7 @@ struct IPadSkillWorkshopScreen: View {
|
||||
private var agentScopeMenu: some View {
|
||||
HStack(spacing: 8) {
|
||||
Text("Agent")
|
||||
.font(.caption.weight(.semibold))
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.foregroundStyle(.secondary)
|
||||
Menu {
|
||||
Button("Default agent") {
|
||||
@@ -237,10 +237,10 @@ struct IPadSkillWorkshopScreen: View {
|
||||
} label: {
|
||||
HStack(spacing: 6) {
|
||||
Text(self.agentScopeLabel)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.lineLimit(1)
|
||||
Image(systemName: "chevron.up.chevron.down")
|
||||
.font(.caption2.weight(.bold))
|
||||
.font(OpenClawType.caption2Bold)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
}
|
||||
@@ -416,9 +416,9 @@ struct IPadSkillWorkshopScreen: View {
|
||||
ProIconBadge(systemName: "hammer", color: proposal.statusColor)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(proposal.title)
|
||||
.font(.headline)
|
||||
.font(OpenClawType.headline)
|
||||
Text(proposal.description)
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
@@ -432,23 +432,23 @@ struct IPadSkillWorkshopScreen: View {
|
||||
|
||||
if let content = proposal.content, !content.isEmpty {
|
||||
Text(content)
|
||||
.font(.caption.monospaced())
|
||||
.font(OpenClawType.monoSmall)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(16)
|
||||
.textSelection(.enabled)
|
||||
} else {
|
||||
Text("Select refresh to load the proposal body.")
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
if !proposal.supportFiles.isEmpty {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("Support files")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
ForEach(proposal.supportFiles, id: \.path) { file in
|
||||
Text(file.path)
|
||||
.font(.caption2.monospaced())
|
||||
.font(OpenClawType.monoSmall)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
@@ -579,7 +579,7 @@ struct IPadSkillWorkshopScreen: View {
|
||||
Image(systemName: "lock.shield")
|
||||
.foregroundStyle(OpenClawBrand.warn)
|
||||
Text("Admin scope required.")
|
||||
.font(.caption.weight(.semibold))
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.foregroundStyle(.secondary)
|
||||
Spacer(minLength: 8)
|
||||
}
|
||||
@@ -951,11 +951,11 @@ private struct IPadSkillProposalKanbanCard: View {
|
||||
color: self.proposal.statusColor)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(self.proposal.title)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.foregroundStyle(self.isSelected ? OpenClawBrand.accent : .primary)
|
||||
.lineLimit(2)
|
||||
Text(self.proposal.description)
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(3)
|
||||
}
|
||||
@@ -964,7 +964,7 @@ private struct IPadSkillProposalKanbanCard: View {
|
||||
ProValuePill(value: self.proposal.status, color: self.proposal.statusColor)
|
||||
Spacer(minLength: 4)
|
||||
Text(self.proposal.ageLabel)
|
||||
.font(.caption2.weight(.semibold))
|
||||
.font(OpenClawType.caption2SemiBold)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
@@ -1000,10 +1000,10 @@ private struct IPadSkillProposalKanbanCard: View {
|
||||
.disabled(self.isInspecting)
|
||||
}
|
||||
}
|
||||
.padding(12)
|
||||
.padding(OpenClawSpacing.space3)
|
||||
.background(
|
||||
self.isSelected ? OpenClawBrand.accent.opacity(0.08) : Color.clear,
|
||||
in: RoundedRectangle(cornerRadius: 8, style: .continuous))
|
||||
in: RoundedRectangle(cornerRadius: OpenClawRadius.xs, style: .continuous))
|
||||
.contentShape(Rectangle())
|
||||
.contextMenu {
|
||||
Button("Inspect", action: self.inspect)
|
||||
@@ -1027,24 +1027,24 @@ struct IPadSkillProposalRow: View {
|
||||
ProIconBadge(systemName: self.isBusy ? "hourglass" : "hammer", color: self.proposal.statusColor)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(self.proposal.title)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.foregroundStyle(self.isSelected ? OpenClawBrand.accent : .primary)
|
||||
.lineLimit(1)
|
||||
Text(self.proposal.description)
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
}
|
||||
Spacer(minLength: 8)
|
||||
Text(self.proposal.ageLabel)
|
||||
.font(.caption2.weight(.semibold))
|
||||
.font(OpenClawType.caption2SemiBold)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.vertical, 10)
|
||||
.background(
|
||||
self.isSelected ? OpenClawBrand.danger.opacity(0.08) : Color.clear,
|
||||
in: RoundedRectangle(cornerRadius: 8, style: .continuous))
|
||||
in: RoundedRectangle(cornerRadius: OpenClawRadius.xs, style: .continuous))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -105,12 +105,12 @@ struct IPadWorkboardScreen: View {
|
||||
self.boardScopeMenu
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.font(.caption.weight(.semibold))
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.foregroundStyle(.secondary)
|
||||
TextField("Search cards", text: self.$query)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
.font(.subheadline)
|
||||
.font(OpenClawType.subhead)
|
||||
if !self.query.isEmpty {
|
||||
Button {
|
||||
self.query = ""
|
||||
@@ -164,12 +164,12 @@ struct IPadWorkboardScreen: View {
|
||||
|
||||
if let dispatchSummaryText {
|
||||
Text(dispatchSummaryText)
|
||||
.font(.caption2)
|
||||
.font(OpenClawType.caption2)
|
||||
.foregroundStyle(OpenClawBrand.accent)
|
||||
}
|
||||
if let errorText {
|
||||
Text(errorText)
|
||||
.font(.caption2)
|
||||
.font(OpenClawType.caption2)
|
||||
.foregroundStyle(OpenClawBrand.warn)
|
||||
}
|
||||
}
|
||||
@@ -182,7 +182,7 @@ struct IPadWorkboardScreen: View {
|
||||
VStack(alignment: .leading, spacing: 9) {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 10) {
|
||||
Text("\(self.filteredCards.count) cards")
|
||||
.font(.headline)
|
||||
.font(OpenClawType.headline)
|
||||
Spacer(minLength: 8)
|
||||
self.compactRefreshButton
|
||||
}
|
||||
@@ -206,19 +206,19 @@ struct IPadWorkboardScreen: View {
|
||||
}
|
||||
} else {
|
||||
Text(Self.compactWriteUnavailableMessage(canRead: self.canRead))
|
||||
.font(.caption2)
|
||||
.font(OpenClawType.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
}
|
||||
|
||||
if let dispatchSummaryText {
|
||||
Text(dispatchSummaryText)
|
||||
.font(.caption2)
|
||||
.font(OpenClawType.caption2)
|
||||
.foregroundStyle(OpenClawBrand.accent)
|
||||
}
|
||||
if let errorText {
|
||||
Text(errorText)
|
||||
.font(.caption2)
|
||||
.font(OpenClawType.caption2)
|
||||
.foregroundStyle(OpenClawBrand.warn)
|
||||
}
|
||||
}
|
||||
@@ -231,7 +231,7 @@ struct IPadWorkboardScreen: View {
|
||||
Task { await self.loadCards(force: true) }
|
||||
} label: {
|
||||
Image(systemName: "arrow.clockwise")
|
||||
.font(.caption.weight(.semibold))
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.frame(width: 32, height: 32)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
@@ -266,19 +266,21 @@ struct IPadWorkboardScreen: View {
|
||||
} label: {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "rectangle.stack")
|
||||
.font(.caption.weight(.semibold))
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
Text(self.boardScopeLabel)
|
||||
.font(.caption.weight(.semibold))
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.lineLimit(1)
|
||||
Spacer(minLength: 4)
|
||||
Image(systemName: "chevron.up.chevron.down")
|
||||
.font(.caption2.weight(.bold))
|
||||
.font(OpenClawType.caption2Bold)
|
||||
}
|
||||
.padding(.horizontal, 10)
|
||||
.frame(height: 32)
|
||||
.background(Color.primary.opacity(0.06), in: RoundedRectangle(cornerRadius: 8, style: .continuous))
|
||||
.background(
|
||||
Color.primary.opacity(0.06),
|
||||
in: RoundedRectangle(cornerRadius: OpenClawRadius.xs, style: .continuous))
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: 8, style: .continuous)
|
||||
RoundedRectangle(cornerRadius: OpenClawRadius.xs, style: .continuous)
|
||||
.strokeBorder(Color.primary.opacity(0.08), lineWidth: 1)
|
||||
}
|
||||
}
|
||||
@@ -313,7 +315,7 @@ struct IPadWorkboardScreen: View {
|
||||
self.selectedStatus = status
|
||||
} label: {
|
||||
Text(IPadWorkboardDefaults.label(for: status))
|
||||
.font(.caption2.weight(.semibold))
|
||||
.font(OpenClawType.caption2SemiBold)
|
||||
.lineLimit(1)
|
||||
.padding(.horizontal, 10)
|
||||
.frame(height: 30)
|
||||
@@ -339,7 +341,7 @@ struct IPadWorkboardScreen: View {
|
||||
private var boardScopeMenu: some View {
|
||||
HStack(spacing: 8) {
|
||||
Text("Board")
|
||||
.font(.caption.weight(.semibold))
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.foregroundStyle(.secondary)
|
||||
Menu {
|
||||
Button("All boards") {
|
||||
@@ -353,10 +355,10 @@ struct IPadWorkboardScreen: View {
|
||||
} label: {
|
||||
HStack(spacing: 6) {
|
||||
Text(self.boardScopeLabel)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.lineLimit(1)
|
||||
Image(systemName: "chevron.up.chevron.down")
|
||||
.font(.caption2.weight(.bold))
|
||||
.font(OpenClawType.caption2Bold)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
}
|
||||
@@ -370,7 +372,7 @@ struct IPadWorkboardScreen: View {
|
||||
private var statusMenu: some View {
|
||||
HStack(spacing: 8) {
|
||||
Text("Status")
|
||||
.font(.caption.weight(.semibold))
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.foregroundStyle(.secondary)
|
||||
Menu {
|
||||
Button("Active") {
|
||||
@@ -384,9 +386,9 @@ struct IPadWorkboardScreen: View {
|
||||
} label: {
|
||||
HStack(spacing: 6) {
|
||||
Text(IPadWorkboardDefaults.label(for: self.selectedStatus))
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
Image(systemName: "chevron.up.chevron.down")
|
||||
.font(.caption2.weight(.bold))
|
||||
.font(OpenClawType.caption2Bold)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
}
|
||||
@@ -975,10 +977,10 @@ private struct IPadWorkboardKanbanCard: View {
|
||||
ProIconBadge(systemName: self.icon, color: self.color)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(self.card.title)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.lineLimit(2)
|
||||
Text(self.detail)
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(3)
|
||||
}
|
||||
@@ -986,7 +988,7 @@ private struct IPadWorkboardKanbanCard: View {
|
||||
|
||||
if !self.card.labels.isEmpty {
|
||||
Text(self.card.labels.prefix(3).joined(separator: ", "))
|
||||
.font(.caption2.weight(.medium))
|
||||
.font(OpenClawType.caption2Medium)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
@@ -1025,7 +1027,7 @@ private struct IPadWorkboardKanbanCard: View {
|
||||
ProValuePill(value: IPadWorkboardDefaults.label(for: self.card.status), color: self.color)
|
||||
}
|
||||
}
|
||||
.padding(12)
|
||||
.padding(OpenClawSpacing.space3)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
|
||||
@@ -1078,10 +1080,10 @@ struct IPadWorkboardQueueRow: View {
|
||||
ProIconBadge(systemName: self.icon, color: self.color)
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text(self.card.title)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.lineLimit(2)
|
||||
Text(self.detail)
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
}
|
||||
@@ -1097,7 +1099,7 @@ struct IPadWorkboardQueueRow: View {
|
||||
self.actionMenuItems
|
||||
} label: {
|
||||
Image(systemName: self.isBusy ? "hourglass" : "ellipsis.circle")
|
||||
.font(.system(size: 19, weight: .semibold))
|
||||
.font(OpenClawType.headline)
|
||||
.frame(width: 36, height: 36)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
|
||||
@@ -61,6 +61,13 @@ enum OpenClawBrand {
|
||||
static let uiAccentForeground = adaptiveUIColor(light: (183, 56, 51), dark: (255, 107, 102))
|
||||
static let uiAccentHot = adaptiveUIColor(light: (204, 75, 69), dark: (232, 92, 86))
|
||||
static let uiAccentHotForeground = adaptiveUIColor(light: (166, 55, 50), dark: (255, 123, 115))
|
||||
static let uiAccentPressed = adaptiveUIColor(light: (148, 40, 36), dark: (148, 40, 36))
|
||||
static let uiTeal = adaptiveUIColor(light: (0, 196, 176), dark: (0, 196, 176))
|
||||
static let uiVoid = adaptiveUIColor(light: (246, 247, 249), dark: (11, 12, 17))
|
||||
static let uiObsidian = adaptiveUIColor(light: (255, 255, 255), dark: (19, 21, 28))
|
||||
static let uiSlate = adaptiveUIColor(light: (242, 243, 247), dark: (28, 31, 43))
|
||||
static let uiTextPrimary = adaptiveUIColor(light: (11, 12, 17), dark: (242, 239, 232))
|
||||
static let uiTextSecondary = adaptiveUIColor(light: (90, 94, 110), dark: (168, 170, 191))
|
||||
static let uiOK = adaptiveUIColor(light: (19, 122, 62), dark: (48, 209, 88))
|
||||
static let uiWarn = adaptiveUIColor(light: (154, 87, 0), dark: (255, 214, 10))
|
||||
static let uiDanger = adaptiveUIColor(light: (185, 28, 28), dark: (252, 165, 165))
|
||||
@@ -70,12 +77,19 @@ enum OpenClawBrand {
|
||||
static let accentForeground = Color(uiColor: Self.uiAccentForeground)
|
||||
static let accentHot = Color(uiColor: Self.uiAccentHot)
|
||||
static let accentHotForeground = Color(uiColor: Self.uiAccentHotForeground)
|
||||
static let accentPressed = Color(uiColor: Self.uiAccentPressed)
|
||||
static let teal = Color(uiColor: Self.uiTeal)
|
||||
static let void = Color(uiColor: Self.uiVoid)
|
||||
static let obsidian = Color(uiColor: Self.uiObsidian)
|
||||
static let slate = Color(uiColor: Self.uiSlate)
|
||||
static let textPrimary = Color(uiColor: Self.uiTextPrimary)
|
||||
static let textSecondary = Color(uiColor: Self.uiTextSecondary)
|
||||
static let danger = Color(uiColor: Self.uiDanger)
|
||||
static let ok = Color(uiColor: Self.uiOK)
|
||||
static let warn = Color(uiColor: Self.uiWarn)
|
||||
static let info = Color(uiColor: Self.uiInfo)
|
||||
static let graphite = Color(uiColor: adaptiveUIColor(light: (246, 247, 249), dark: (11, 12, 17)))
|
||||
static let graphiteElevated = Color(uiColor: adaptiveUIColor(light: (255, 255, 255), dark: (19, 21, 28)))
|
||||
static let graphite = void
|
||||
static let graphiteElevated = obsidian
|
||||
|
||||
static var sheetBackground: LinearGradient {
|
||||
LinearGradient(
|
||||
|
||||
@@ -112,15 +112,15 @@ struct OpenClawDocsScreen: View {
|
||||
ProIconBadge(systemName: icon, color: OpenClawBrand.accent)
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(title)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
Text(detail)
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
Spacer(minLength: 8)
|
||||
Image(systemName: "arrow.up.right")
|
||||
.font(.caption.weight(.bold))
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(.horizontal, 14)
|
||||
|
||||
@@ -8,6 +8,23 @@ enum OpenClawProMetric {
|
||||
static let bottomScrollInset: CGFloat = 96
|
||||
}
|
||||
|
||||
enum OpenClawSpacing {
|
||||
static let space1: CGFloat = 4
|
||||
static let space2: CGFloat = 8
|
||||
static let space3: CGFloat = 12
|
||||
static let space4: CGFloat = 16
|
||||
static let space6: CGFloat = 24
|
||||
static let space8: CGFloat = 32
|
||||
}
|
||||
|
||||
enum OpenClawRadius {
|
||||
static let xs: CGFloat = 8
|
||||
static let sm: CGFloat = 10
|
||||
static let md: CGFloat = 12
|
||||
static let lg: CGFloat = 16
|
||||
static let xl: CGFloat = 20
|
||||
}
|
||||
|
||||
struct OpenClawProBackground: View {
|
||||
var body: some View {
|
||||
Color(uiColor: .systemGroupedBackground)
|
||||
@@ -24,18 +41,18 @@ struct ProSectionHeader: View {
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text(self.title)
|
||||
.font(.footnote.weight(.medium))
|
||||
.font(OpenClawType.footnoteMedium)
|
||||
.foregroundStyle(.secondary)
|
||||
.textCase(self.uppercase ? .uppercase : nil)
|
||||
Spacer()
|
||||
if let actionTitle {
|
||||
if let action {
|
||||
Button(actionTitle, action: action)
|
||||
.font(.footnote.weight(.medium))
|
||||
.foregroundStyle(OpenClawBrand.accentForeground)
|
||||
.font(OpenClawType.footnoteMedium)
|
||||
.foregroundStyle(OpenClawBrand.accent)
|
||||
} else {
|
||||
Text(actionTitle)
|
||||
.font(.footnote.weight(.medium))
|
||||
.font(OpenClawType.footnoteMedium)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
@@ -69,7 +86,7 @@ private struct ProPanelBackground: View {
|
||||
let isProminent: Bool
|
||||
|
||||
var body: some View {
|
||||
let shape = RoundedRectangle(cornerRadius: self.radius, style: .continuous)
|
||||
let shape = RoundedRectangle(cornerRadius: radius, style: .continuous)
|
||||
shape
|
||||
.fill(self.fill)
|
||||
.overlay {
|
||||
@@ -96,7 +113,7 @@ private struct ProInsetSurfaceModifier: ViewModifier {
|
||||
let radius: CGFloat
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
let shape = RoundedRectangle(cornerRadius: self.radius, style: .continuous)
|
||||
let shape = RoundedRectangle(cornerRadius: radius, style: .continuous)
|
||||
content.background {
|
||||
shape
|
||||
.fill(Color(uiColor: .tertiarySystemGroupedBackground))
|
||||
@@ -166,26 +183,26 @@ extension View {
|
||||
radius: CGFloat = OpenClawProMetric.cardRadius,
|
||||
isProminent: Bool = false) -> some View
|
||||
{
|
||||
self.modifier(ProPanelSurfaceModifier(
|
||||
modifier(ProPanelSurfaceModifier(
|
||||
tint: tint,
|
||||
radius: radius,
|
||||
isProminent: isProminent))
|
||||
}
|
||||
|
||||
func proInsetSurface(tint: Color, radius: CGFloat) -> some View {
|
||||
self.modifier(ProInsetSurfaceModifier(tint: tint, radius: radius))
|
||||
modifier(ProInsetSurfaceModifier(tint: tint, radius: radius))
|
||||
}
|
||||
|
||||
func openClawGlassButton(prominent: Bool = false, tint: Color? = nil) -> some View {
|
||||
self.modifier(OpenClawGlassButtonModifier(prominent: prominent, tint: tint))
|
||||
modifier(OpenClawGlassButtonModifier(prominent: prominent, tint: tint))
|
||||
}
|
||||
|
||||
func openClawTabBarBehavior() -> some View {
|
||||
self.modifier(OpenClawTabBarBehaviorModifier())
|
||||
modifier(OpenClawTabBarBehaviorModifier())
|
||||
}
|
||||
|
||||
func openClawGlassSurface(radius: CGFloat = OpenClawProMetric.controlRadius) -> some View {
|
||||
self.modifier(OpenClawGlassSurfaceModifier(radius: radius))
|
||||
modifier(OpenClawGlassSurfaceModifier(radius: radius))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,11 +235,11 @@ struct ProIconBadge: View {
|
||||
|
||||
var body: some View {
|
||||
Image(systemName: self.systemName)
|
||||
.font(.caption.weight(.semibold))
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.foregroundStyle(self.color)
|
||||
.frame(width: 30, height: 30)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 8, style: .continuous)
|
||||
RoundedRectangle(cornerRadius: OpenClawRadius.xs, style: .continuous)
|
||||
.fill(self.color.opacity(0.12))
|
||||
}
|
||||
}
|
||||
@@ -255,9 +272,9 @@ struct OpenClawSidebarRevealButton: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
let button = Button(action: self.headerAction.action) {
|
||||
let button = Button(action: headerAction.action) {
|
||||
Image(systemName: self.headerAction.systemName)
|
||||
.font(.system(size: 16, weight: .semibold))
|
||||
.font(OpenClawType.subhead)
|
||||
.frame(
|
||||
width: OpenClawProMetric.compactControlSize,
|
||||
height: OpenClawProMetric.compactControlSize)
|
||||
@@ -267,7 +284,7 @@ struct OpenClawSidebarRevealButton: View {
|
||||
.openClawGlassButton(tint: OpenClawBrand.accent)
|
||||
.accessibilityLabel(self.headerAction.accessibilityLabel)
|
||||
|
||||
if let accessibilityIdentifier = self.headerAction.accessibilityIdentifier {
|
||||
if let accessibilityIdentifier = headerAction.accessibilityIdentifier {
|
||||
button.accessibilityIdentifier(accessibilityIdentifier)
|
||||
} else {
|
||||
button
|
||||
@@ -324,16 +341,16 @@ struct OpenClawNoticeBanner: View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
||||
Text(self.title)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.multilineTextAlignment(.leading)
|
||||
Spacer(minLength: 0)
|
||||
Text(self.ownerLabel)
|
||||
.font(.caption.weight(.semibold))
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Text(self.message)
|
||||
.font(.footnote)
|
||||
.font(OpenClawType.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
@@ -367,12 +384,12 @@ struct OpenClawNoticeBanner: View {
|
||||
switch detail {
|
||||
case let .accent(value):
|
||||
Text(value)
|
||||
.font(.caption.weight(.medium))
|
||||
.font(OpenClawType.captionMedium)
|
||||
.foregroundStyle(self.tint)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
case let .requestID(value):
|
||||
Text("Request ID: \(value)")
|
||||
.font(.system(.caption, design: .monospaced).weight(.medium))
|
||||
.font(OpenClawType.monoSmall)
|
||||
.foregroundStyle(.secondary)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
@@ -383,8 +400,8 @@ struct OpenClawNoticeBanner: View {
|
||||
struct OpenClawAdaptiveHeaderRow<Leading: View, Accessory: View>: View {
|
||||
let title: String
|
||||
let subtitle: String?
|
||||
var titleFont: Font = .title3.weight(.semibold)
|
||||
var subtitleFont: Font = .subheadline
|
||||
var titleFont: Font = OpenClawType.title3
|
||||
var subtitleFont: Font = OpenClawType.subhead
|
||||
var subtitleLineLimit: Int? = 2
|
||||
@ViewBuilder let leading: Leading
|
||||
@ViewBuilder let accessory: Accessory
|
||||
@@ -392,8 +409,8 @@ struct OpenClawAdaptiveHeaderRow<Leading: View, Accessory: View>: View {
|
||||
init(
|
||||
title: String,
|
||||
subtitle: String? = nil,
|
||||
titleFont: Font = .title3.weight(.semibold),
|
||||
subtitleFont: Font = .subheadline,
|
||||
titleFont: Font = OpenClawType.title3,
|
||||
subtitleFont: Font = OpenClawType.subhead,
|
||||
subtitleLineLimit: Int? = 2,
|
||||
@ViewBuilder leading: () -> Leading,
|
||||
@ViewBuilder accessory: () -> Accessory)
|
||||
@@ -465,6 +482,117 @@ struct OpenClawAdaptiveHeaderRow<Leading: View, Accessory: View>: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Shared switch indicator replacing the 3 duplicated capsule toggles.
|
||||
/// Native Toggle only hits the switch edge on iOS 26; this full-width button approach
|
||||
/// gives the whole row a tap target.
|
||||
struct OpenClawToggleIndicator: View {
|
||||
let isOn: Bool
|
||||
|
||||
var body: some View {
|
||||
Capsule()
|
||||
.fill(self.isOn ? OpenClawBrand.accent : Color.secondary.opacity(0.35))
|
||||
.frame(width: 52, height: 32)
|
||||
.overlay(alignment: self.isOn ? .trailing : .leading) {
|
||||
Circle()
|
||||
.fill(Color.white)
|
||||
.frame(width: 28, height: 28)
|
||||
.padding(2)
|
||||
.shadow(color: Color.black.opacity(0.14), radius: 1, x: 0, y: 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum OpenClawStatusTone {
|
||||
case ok
|
||||
case warn
|
||||
case danger
|
||||
case info
|
||||
case accent
|
||||
case teal
|
||||
case muted
|
||||
|
||||
var color: Color {
|
||||
switch self {
|
||||
case .ok: OpenClawBrand.ok
|
||||
case .warn: OpenClawBrand.warn
|
||||
case .danger: OpenClawBrand.danger
|
||||
case .info: OpenClawBrand.info
|
||||
case .accent: OpenClawBrand.accent
|
||||
case .teal: OpenClawBrand.teal
|
||||
case .muted: OpenClawBrand.textSecondary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct OpenClawStatusBadge: View {
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
let label: String
|
||||
let tone: OpenClawStatusTone
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: OpenClawSpacing.space1 + 2) {
|
||||
Circle()
|
||||
.fill(self.tone.color)
|
||||
.frame(width: 7, height: 7)
|
||||
.shadow(color: self.tone.color.opacity(0.55), radius: 3)
|
||||
Text(self.label)
|
||||
.font(OpenClawType.caption2SemiBold)
|
||||
.foregroundStyle(self.tone.color)
|
||||
}
|
||||
.padding(.horizontal, OpenClawSpacing.space2)
|
||||
.padding(.vertical, 5)
|
||||
.background {
|
||||
Capsule()
|
||||
.fill(self.tone.color.opacity(self.colorScheme == .dark ? 0.14 : 0.10))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct OpenClawPrimaryButtonStyle: ButtonStyle {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.font(OpenClawType.headline)
|
||||
.foregroundStyle(.white)
|
||||
.frame(maxWidth: .infinity, minHeight: 48)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: OpenClawRadius.sm, style: .continuous)
|
||||
.fill(
|
||||
configuration.isPressed
|
||||
? OpenClawBrand.accentPressed
|
||||
: OpenClawBrand.accent)
|
||||
}
|
||||
.animation(.easeOut(duration: 0.15), value: configuration.isPressed)
|
||||
}
|
||||
}
|
||||
|
||||
struct OpenClawSecondaryButtonStyle: ButtonStyle {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.font(OpenClawType.headline)
|
||||
.foregroundStyle(OpenClawBrand.textPrimary)
|
||||
.frame(maxWidth: .infinity, minHeight: 48)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: OpenClawRadius.sm, style: .continuous)
|
||||
.fill(Color(uiColor: .secondarySystemBackground))
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: OpenClawRadius.sm, style: .continuous)
|
||||
.strokeBorder(Color(uiColor: .separator).opacity(0.35), lineWidth: 1)
|
||||
}
|
||||
}
|
||||
.opacity(configuration.isPressed ? 0.82 : 1)
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func openClawPrimaryButton() -> some View {
|
||||
self.buttonStyle(OpenClawPrimaryButtonStyle())
|
||||
}
|
||||
|
||||
func openClawSecondaryButton() -> some View {
|
||||
self.buttonStyle(OpenClawSecondaryButtonStyle())
|
||||
}
|
||||
}
|
||||
|
||||
struct ProStatusDot: View {
|
||||
var color: Color
|
||||
|
||||
@@ -482,7 +610,7 @@ struct ProValuePill: View {
|
||||
|
||||
var body: some View {
|
||||
Text(self.value)
|
||||
.font(.footnote.weight(.semibold))
|
||||
.font(OpenClawType.footnoteSemiBold)
|
||||
.foregroundStyle(self.color)
|
||||
.lineLimit(1)
|
||||
.padding(.horizontal, 8)
|
||||
@@ -531,18 +659,8 @@ struct OpenClawGatewayCompactPill: View {
|
||||
@Environment(NodeAppModel.self) private var appModel
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 6) {
|
||||
Image(systemName: self.icon)
|
||||
.font(.caption.weight(.semibold))
|
||||
Text(self.title)
|
||||
.font(.caption.weight(.semibold))
|
||||
.lineLimit(1)
|
||||
}
|
||||
.foregroundStyle(self.color)
|
||||
.padding(.horizontal, 4)
|
||||
.frame(minHeight: 30)
|
||||
.fixedSize(horizontal: true, vertical: false)
|
||||
.accessibilityLabel("Gateway \(self.title)")
|
||||
OpenClawStatusBadge(label: self.title, tone: self.tone)
|
||||
.accessibilityLabel("Gateway \(self.title)")
|
||||
}
|
||||
|
||||
private var title: String {
|
||||
@@ -558,29 +676,16 @@ struct OpenClawGatewayCompactPill: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var color: Color {
|
||||
private var tone: OpenClawStatusTone {
|
||||
switch GatewayStatusBuilder.build(appModel: self.appModel) {
|
||||
case .connected:
|
||||
OpenClawBrand.ok
|
||||
.ok
|
||||
case .connecting:
|
||||
OpenClawBrand.accent
|
||||
.accent
|
||||
case .error:
|
||||
OpenClawBrand.warn
|
||||
.warn
|
||||
case .disconnected:
|
||||
.secondary
|
||||
}
|
||||
}
|
||||
|
||||
private var icon: String {
|
||||
switch GatewayStatusBuilder.build(appModel: self.appModel) {
|
||||
case .connected:
|
||||
"checkmark.circle.fill"
|
||||
case .connecting:
|
||||
"arrow.triangle.2.circlepath"
|
||||
case .error:
|
||||
"exclamationmark.triangle.fill"
|
||||
case .disconnected:
|
||||
"wifi.slash"
|
||||
.muted
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -596,7 +701,7 @@ struct ProMetricTile: View {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
HStack {
|
||||
Image(systemName: self.icon)
|
||||
.font(.caption.weight(.semibold))
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.foregroundStyle(self.color)
|
||||
.frame(width: 24, height: 24)
|
||||
.background(self.color.opacity(self.colorScheme == .dark ? 0.18 : 0.10), in: Circle())
|
||||
@@ -605,11 +710,11 @@ struct ProMetricTile: View {
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(self.value)
|
||||
.font(.headline.weight(.bold))
|
||||
.font(OpenClawType.headline)
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.72)
|
||||
Text(self.title)
|
||||
.font(.caption2.weight(.medium))
|
||||
.font(OpenClawType.caption2Medium)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
@@ -666,10 +771,10 @@ struct ProPanelHeader: View {
|
||||
var body: some View {
|
||||
HStack(spacing: 8) {
|
||||
Text(self.title)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
if let value {
|
||||
Text(value)
|
||||
.font(.caption2.weight(.bold))
|
||||
.font(OpenClawType.caption2Bold)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer(minLength: 8)
|
||||
@@ -687,11 +792,11 @@ struct ProPanelHeader: View {
|
||||
Button(action: action) {
|
||||
Image(systemName: actionIcon)
|
||||
}
|
||||
.accessibilityLabel(self.actionAccessibilityLabel ?? self.actionTitle ?? self.title)
|
||||
.accessibilityLabel(self.actionAccessibilityLabel ?? actionTitle ?? self.title)
|
||||
.disabled(self.isActionDisabled)
|
||||
} else if let actionTitle {
|
||||
Button(actionTitle, action: action)
|
||||
.font(.caption.weight(.semibold))
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.disabled(self.isActionDisabled)
|
||||
}
|
||||
}
|
||||
@@ -712,10 +817,10 @@ struct ProStatusRow: View {
|
||||
ProIconBadge(systemName: self.icon, color: self.color)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(self.title)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.lineLimit(1)
|
||||
Text(self.detail)
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
}
|
||||
@@ -726,7 +831,7 @@ struct ProStatusRow: View {
|
||||
}
|
||||
if let actionTitle, let action {
|
||||
Button(actionTitle, action: action)
|
||||
.font(.caption.weight(.semibold))
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.buttonStyle(.bordered)
|
||||
.controlSize(.mini)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
enum OpenClawType {
|
||||
// Display — Plus Jakarta Sans
|
||||
static let title1 = scaledDisplay(name: Display.extraBold, size: 34, relativeTo: .largeTitle)
|
||||
static let title2 = scaledDisplay(name: Display.bold, size: 28, relativeTo: .title1)
|
||||
static let title3 = scaledDisplay(name: Display.bold, size: 22, relativeTo: .title2)
|
||||
static let headline = scaledDisplay(name: Display.semiBold, size: 17, relativeTo: .headline)
|
||||
// Body — DM Sans
|
||||
static let body = scaledBody(name: Body.regular, size: 17, relativeTo: .body)
|
||||
static let callout = scaledBody(name: Body.regular, size: 16, relativeTo: .callout)
|
||||
static let subhead = scaledBody(name: Body.medium, size: 15, relativeTo: .subheadline)
|
||||
static let subheadSemiBold = scaledDisplay(name: Display.semiBold, size: 15, relativeTo: .subheadline)
|
||||
static let footnote = scaledBody(name: Body.regular, size: 13, relativeTo: .footnote)
|
||||
static let footnoteMedium = scaledBody(name: Body.medium, size: 13, relativeTo: .footnote)
|
||||
static let footnoteSemiBold = scaledBody(name: Body.semiBold, size: 13, relativeTo: .footnote)
|
||||
static let caption = scaledBody(name: Body.regular, size: 12, relativeTo: .caption1)
|
||||
static let captionMedium = scaledBody(name: Body.medium, size: 12, relativeTo: .caption1)
|
||||
static let captionSemiBold = scaledBody(name: Body.semiBold, size: 12, relativeTo: .caption1)
|
||||
static let caption2 = scaledBody(name: Body.regular, size: 11, relativeTo: .caption2)
|
||||
static let caption2Medium = scaledBody(name: Body.medium, size: 11, relativeTo: .caption2)
|
||||
static let caption2SemiBold = scaledBody(name: Body.semiBold, size: 11, relativeTo: .caption2)
|
||||
static let caption2Bold = scaledDisplay(name: Display.bold, size: 11, relativeTo: .caption2)
|
||||
static let title2SemiBold = scaledDisplay(name: Display.semiBold, size: 28, relativeTo: .title1)
|
||||
|
||||
// Mono — JetBrains Mono
|
||||
static let mono = scaledMono(name: Mono.regular, size: 14, relativeTo: .body)
|
||||
static let monoSmall = scaledMono(name: Mono.regular, size: 12, relativeTo: .caption1)
|
||||
static let monoFootnote = scaledMono(name: Mono.regular, size: 13, relativeTo: .footnote)
|
||||
static let monoHeadline = scaledMono(name: Mono.medium, size: 17, relativeTo: .headline)
|
||||
|
||||
/// PostScript names for bundled fonts. Keep aligned with `UIAppFonts` in `project.yml`.
|
||||
static let registeredPostScriptNames: [String] = [
|
||||
Display.light,
|
||||
Display.regular,
|
||||
Display.medium,
|
||||
Display.semiBold,
|
||||
Display.bold,
|
||||
Display.extraBold,
|
||||
Body.light,
|
||||
Body.regular,
|
||||
Body.italic,
|
||||
Body.medium,
|
||||
Body.semiBold,
|
||||
Mono.regular,
|
||||
Mono.medium,
|
||||
Mono.semiBold,
|
||||
]
|
||||
|
||||
private enum Display {
|
||||
static let light = "PlusJakartaSans-Light"
|
||||
static let regular = "PlusJakartaSans-Regular"
|
||||
static let medium = "PlusJakartaSans-Medium"
|
||||
static let semiBold = "PlusJakartaSans-SemiBold"
|
||||
static let bold = "PlusJakartaSans-Bold"
|
||||
static let extraBold = "PlusJakartaSans-ExtraBold"
|
||||
}
|
||||
|
||||
private enum Body {
|
||||
static let light = "DMSans-Light"
|
||||
static let regular = "DMSans-Regular"
|
||||
static let italic = "DMSans-Italic"
|
||||
static let medium = "DMSans-Medium"
|
||||
static let semiBold = "DMSans-SemiBold"
|
||||
}
|
||||
|
||||
private enum Mono {
|
||||
static let regular = "JetBrainsMono-Regular"
|
||||
static let medium = "JetBrainsMono-Medium"
|
||||
static let semiBold = "JetBrainsMono-SemiBold"
|
||||
}
|
||||
|
||||
private static func scaledDisplay(
|
||||
name: String,
|
||||
size: CGFloat,
|
||||
relativeTo textStyle: UIFont.TextStyle) -> Font
|
||||
{
|
||||
self.scaledFont(name: name, size: size, relativeTo: textStyle)
|
||||
}
|
||||
|
||||
private static func scaledBody(
|
||||
name: String,
|
||||
size: CGFloat,
|
||||
relativeTo textStyle: UIFont.TextStyle) -> Font
|
||||
{
|
||||
self.scaledFont(name: name, size: size, relativeTo: textStyle)
|
||||
}
|
||||
|
||||
private static func scaledMono(
|
||||
name: String,
|
||||
size: CGFloat,
|
||||
relativeTo textStyle: UIFont.TextStyle) -> Font
|
||||
{
|
||||
self.scaledFont(name: name, size: size, relativeTo: textStyle)
|
||||
}
|
||||
|
||||
private static func scaledFont(
|
||||
name: String,
|
||||
size: CGFloat,
|
||||
relativeTo textStyle: UIFont.TextStyle) -> Font
|
||||
{
|
||||
let base = UIFont(name: name, size: size) ?? UIFont.systemFont(ofSize: size)
|
||||
let scaled = UIFontMetrics(forTextStyle: textStyle).scaledFont(for: base)
|
||||
return Font(scaled)
|
||||
}
|
||||
}
|
||||
@@ -36,9 +36,9 @@ struct SettingsChannelsDestination: View {
|
||||
ProIconBadge(systemName: "point.3.connected.trianglepath.dotted", color: self.summaryColor)
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text("Channels / Integrations")
|
||||
.font(.headline)
|
||||
.font(OpenClawType.headline)
|
||||
Text(self.summaryDetail)
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
@@ -346,14 +346,14 @@ private struct SettingsChannelRow: View {
|
||||
ProIconBadge(systemName: self.entry.systemImage, color: self.entry.color)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(self.entry.label)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
Text(self.entry.detailText)
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
if let lastError = self.entry.lastError {
|
||||
Text(lastError)
|
||||
.font(.caption2.weight(.medium))
|
||||
.font(OpenClawType.caption2Medium)
|
||||
.foregroundStyle(OpenClawBrand.warn)
|
||||
.lineLimit(2)
|
||||
}
|
||||
@@ -384,9 +384,9 @@ private struct SettingsChannelRow: View {
|
||||
.frame(width: 28, height: 28)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(account.displayName)
|
||||
.font(.caption.weight(.semibold))
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
Text(account.detailText)
|
||||
.font(.caption2)
|
||||
.font(OpenClawType.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
@@ -628,7 +628,7 @@ private struct SettingsChannelsStatesPreview: View {
|
||||
{
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(title)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.foregroundStyle(.secondary)
|
||||
ProCard(padding: 0, radius: SettingsLayout.cardRadius) {
|
||||
VStack(spacing: 0) {
|
||||
|
||||
@@ -285,12 +285,12 @@ struct HostedPushRelayDisclosureSheet: View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 18) {
|
||||
Image(systemName: "network")
|
||||
.font(.title2.weight(.semibold))
|
||||
.font(OpenClawType.title2SemiBold)
|
||||
.foregroundStyle(OpenClawBrand.accentForeground)
|
||||
Text("Enable OpenClaw Hosted Push Relay?")
|
||||
.font(.title3.weight(.semibold))
|
||||
.font(OpenClawType.title3)
|
||||
Text(self.message)
|
||||
.font(.body)
|
||||
.font(OpenClawType.body)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
@@ -17,9 +17,9 @@ extension SettingsProTab {
|
||||
ProIconBadge(systemName: icon, color: color)
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(title)
|
||||
.font(.headline)
|
||||
.font(OpenClawType.headline)
|
||||
Text(detail)
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
}
|
||||
@@ -100,9 +100,9 @@ extension SettingsProTab {
|
||||
ProIconBadge(systemName: icon, color: color)
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(title)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
Text(detail)
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
@@ -123,11 +123,11 @@ extension SettingsProTab {
|
||||
func detailRow(_ label: String, value: String) -> some View {
|
||||
HStack {
|
||||
Text(label)
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
Spacer(minLength: 8)
|
||||
Text(value)
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
}
|
||||
@@ -427,7 +427,11 @@ extension SettingsProTab {
|
||||
} else {
|
||||
self.locationModeRaw = previous
|
||||
self.previousLocationModeRaw = previous
|
||||
self.locationStatusText = "Location permission was not granted."
|
||||
if mode == .always {
|
||||
self.locationStatusText = "Always Allow was not granted. Open iOS Settings → OpenClaw → Location to enable it."
|
||||
} else {
|
||||
self.locationStatusText = "Location permission was not granted. Check iOS Settings → OpenClaw → Location."
|
||||
}
|
||||
self.refreshLocationPermissionSummary(
|
||||
desiredMode: OpenClawLocationMode(rawValue: previous) ?? .off)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import CoreLocation
|
||||
import OpenClawKit
|
||||
import SwiftUI
|
||||
|
||||
@@ -76,10 +77,10 @@ extension SettingsProTab {
|
||||
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text("Gateway")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.lineLimit(1)
|
||||
Text(self.gatewaySummaryDetail)
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
@@ -190,10 +191,10 @@ extension SettingsProTab {
|
||||
ProIconBadge(systemName: icon, color: color)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(title)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
if let detail, !detail.isEmpty {
|
||||
Text(detail)
|
||||
.font(.footnote)
|
||||
.font(OpenClawType.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
@@ -315,12 +316,12 @@ extension SettingsProTab {
|
||||
ProIconBadge(systemName: "bell.slash.fill", color: OpenClawBrand.warn)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Notifications are off")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
Text(
|
||||
"""
|
||||
Enable Notifications to receive approval notifications while OpenClaw is not open.
|
||||
""")
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
@@ -356,7 +357,7 @@ extension SettingsProTab {
|
||||
|
||||
if let errorText = self.appModel.pendingExecApprovalPromptErrorText {
|
||||
Text(errorText)
|
||||
.font(.caption2.weight(.medium))
|
||||
.font(OpenClawType.caption2Medium)
|
||||
.foregroundStyle(OpenClawBrand.danger)
|
||||
}
|
||||
|
||||
@@ -397,9 +398,9 @@ extension SettingsProTab {
|
||||
ProIconBadge(systemName: "checkmark.shield.fill", color: OpenClawBrand.ok)
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text("No approvals waiting")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
Text(self.approvalEmptyDetail)
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
}
|
||||
@@ -533,7 +534,7 @@ extension SettingsProTab {
|
||||
.disabled(self.notificationStatus == .checking || self.isRequestingNotificationAuthorization)
|
||||
|
||||
Text(self.notificationStatusDetail)
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
@@ -541,11 +542,11 @@ extension SettingsProTab {
|
||||
|
||||
HStack(alignment: .top, spacing: 10) {
|
||||
Image(systemName: "network")
|
||||
.font(.caption.weight(.semibold))
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.foregroundStyle(OpenClawBrand.accent)
|
||||
.frame(width: 22, height: 22)
|
||||
Text(self.notificationRelayDetail)
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
@@ -641,9 +642,9 @@ extension SettingsProTab {
|
||||
Button(action: action) {
|
||||
HStack(spacing: 7) {
|
||||
Image(systemName: isBusy ? "hourglass" : icon)
|
||||
.font(.caption.weight(.semibold))
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
Text(title)
|
||||
.font(.caption.weight(.semibold))
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.76)
|
||||
}
|
||||
@@ -669,9 +670,9 @@ extension SettingsProTab {
|
||||
ProIconBadge(systemName: icon, color: isOn.wrappedValue ? OpenClawBrand.accent : .secondary)
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(title)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
Text(detail)
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
}
|
||||
@@ -692,9 +693,9 @@ extension SettingsProTab {
|
||||
.accent)
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text("Location")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
Text("Controls whether location can be shared with gateway tools.")
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
}
|
||||
@@ -714,13 +715,17 @@ extension SettingsProTab {
|
||||
.disabled(self.isChangingLocationMode)
|
||||
|
||||
Text(self.locationPermissionDetailText)
|
||||
.font(.caption2)
|
||||
.font(OpenClawType.caption2)
|
||||
.foregroundStyle(
|
||||
self.locationPermissionSummary.needsAttention ? OpenClawBrand.warn : .secondary)
|
||||
|
||||
if self.locationModeRaw == OpenClawLocationMode.always.rawValue {
|
||||
self.locationAlwaysGuidance
|
||||
}
|
||||
|
||||
if let locationPermissionWarningText {
|
||||
Text(locationPermissionWarningText)
|
||||
.font(.caption2)
|
||||
.font(OpenClawType.caption2)
|
||||
.foregroundStyle(OpenClawBrand.warn)
|
||||
}
|
||||
}
|
||||
@@ -728,11 +733,62 @@ extension SettingsProTab {
|
||||
.padding(.horizontal, OpenClawProMetric.pagePadding)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var locationAlwaysGuidance: some View {
|
||||
let osStatus = CLLocationManager().authorizationStatus
|
||||
switch osStatus {
|
||||
case .authorizedAlways:
|
||||
Label {
|
||||
Text("Always Allow is active.")
|
||||
} icon: {
|
||||
Image(systemName: "checkmark.circle")
|
||||
}
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(OpenClawBrand.ok)
|
||||
case .authorizedWhenInUse:
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Label {
|
||||
Text("iOS is set to While Using.")
|
||||
+ Text(" Change to Always in Settings for reliable background automations.")
|
||||
} icon: {
|
||||
Image(systemName: "exclamationmark.triangle")
|
||||
}
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(OpenClawBrand.warn)
|
||||
self.openSettingsButton
|
||||
}
|
||||
case .denied, .restricted:
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Label {
|
||||
Text("Location access is denied.")
|
||||
+ Text(" Enable it in Settings.")
|
||||
} icon: {
|
||||
Image(systemName: "xmark.circle")
|
||||
}
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(OpenClawBrand.danger)
|
||||
self.openSettingsButton
|
||||
}
|
||||
default:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
private var openSettingsButton: some View {
|
||||
Button("Open Settings") {
|
||||
guard let url = URL(
|
||||
string: UIApplication.openSettingsURLString) else { return }
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
.font(OpenClawType.caption)
|
||||
.buttonStyle(.bordered)
|
||||
}
|
||||
|
||||
var agentSelectionCard: some View {
|
||||
ProCard(radius: SettingsLayout.cardRadius) {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text("Default Agent")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
Picker("Agent", selection: self.$selectedAgentPickerId) {
|
||||
Text("Default").tag("")
|
||||
let defaultId = (self.appModel.gatewayDefaultAgentId ?? "")
|
||||
@@ -743,7 +799,7 @@ extension SettingsProTab {
|
||||
}
|
||||
}
|
||||
Text("Used for new Chat and Talk sessions.")
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
@@ -754,7 +810,7 @@ extension SettingsProTab {
|
||||
ProCard(radius: SettingsLayout.cardRadius) {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("Setup Code")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
TextField("Paste setup code", text: self.$setupCode)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
@@ -780,13 +836,13 @@ extension SettingsProTab {
|
||||
}
|
||||
if let status = self.setupStatusLine {
|
||||
Text(status)
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
if let warning = self.tailnetWarningText {
|
||||
Text(warning)
|
||||
.font(.caption.weight(.semibold))
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.foregroundStyle(OpenClawBrand.warn)
|
||||
}
|
||||
}
|
||||
@@ -798,10 +854,10 @@ extension SettingsProTab {
|
||||
ProCard(radius: SettingsLayout.cardRadius) {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("Discovered Gateways")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
if self.gatewayController.gateways.isEmpty {
|
||||
Text("No gateways found yet. Use manual setup if Bonjour is blocked.")
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
} else {
|
||||
ForEach(self.gatewayController.gateways) { gateway in
|
||||
@@ -820,9 +876,9 @@ extension SettingsProTab {
|
||||
HStack(spacing: 12) {
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(verbatim: gateway.name)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
Text(verbatim: self.gatewayDetailLines(gateway).joined(separator: " • "))
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
}
|
||||
@@ -983,7 +1039,7 @@ extension SettingsProTab {
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
Text(self.appModel.lastShareEventText)
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
@@ -1050,7 +1106,7 @@ extension SettingsProTab {
|
||||
Spacer(minLength: 8)
|
||||
self.settingsSwitchIndicator(isOn: isOn.wrappedValue)
|
||||
}
|
||||
.font(.subheadline)
|
||||
.font(OpenClawType.subhead)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
@@ -1083,9 +1139,9 @@ extension SettingsProTab {
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.caption.weight(.semibold))
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.font(.subheadline)
|
||||
.font(OpenClawType.subhead)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ struct SettingsApprovalRow: View {
|
||||
var body: some View {
|
||||
HStack(spacing: 10) {
|
||||
Image(systemName: self.item.icon)
|
||||
.font(.caption.weight(.bold))
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.foregroundStyle(.white)
|
||||
.frame(width: 30, height: 30)
|
||||
.background {
|
||||
@@ -45,16 +45,16 @@ struct SettingsApprovalRow: View {
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(self.item.title)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.lineLimit(1)
|
||||
Text(self.item.detail)
|
||||
.font(.caption2.weight(.medium))
|
||||
.font(OpenClawType.caption2Medium)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
Spacer(minLength: 8)
|
||||
Text(self.item.priority)
|
||||
.font(.caption.weight(.bold))
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.foregroundStyle(self.item.color)
|
||||
.padding(.horizontal, 9)
|
||||
.padding(.vertical, 5)
|
||||
@@ -298,7 +298,7 @@ private struct SettingsGatewayStatesPreview: View {
|
||||
{
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(title)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.foregroundStyle(.secondary)
|
||||
content()
|
||||
}
|
||||
@@ -344,11 +344,11 @@ private struct SettingsGatewayStatesPreview: View {
|
||||
private func factRow(_ label: String, value: String) -> some View {
|
||||
HStack {
|
||||
Text(label)
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
Spacer(minLength: 8)
|
||||
Text(value)
|
||||
.font(.caption.weight(.medium))
|
||||
.font(OpenClawType.captionMedium)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
}
|
||||
@@ -372,7 +372,7 @@ private struct SettingsGatewayStatesPreview: View {
|
||||
self.previewButton("Connect", systemImage: "link", isBusy: false)
|
||||
}
|
||||
Text("Discovered gateways and manual setup live here when the gateway has not connected yet.")
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,17 +134,17 @@ struct TalkProTab: View {
|
||||
|
||||
VStack(spacing: 5) {
|
||||
Text(self.state.title)
|
||||
.font(.title3.weight(.bold))
|
||||
.font(OpenClawType.title3)
|
||||
.multilineTextAlignment(.center)
|
||||
Text(self.heroSubtitle)
|
||||
.font(.subheadline.weight(.medium))
|
||||
.font(OpenClawType.subhead)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
|
||||
Button(action: self.handlePrimaryAction) {
|
||||
Label(self.state.primaryButtonTitle, systemImage: self.state.primaryButtonIcon)
|
||||
.font(.subheadline.weight(.bold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 50)
|
||||
}
|
||||
@@ -171,7 +171,7 @@ struct TalkProTab: View {
|
||||
accessibilityIdentifier: "talk-background-listening-control")
|
||||
Button(action: self.openVoiceSettings) {
|
||||
Image(systemName: "slider.horizontal.3")
|
||||
.font(.system(size: 17, weight: .semibold))
|
||||
.font(OpenClawType.headline)
|
||||
.frame(width: 44, height: 44)
|
||||
}
|
||||
.buttonBorderShape(.circle)
|
||||
@@ -194,7 +194,7 @@ struct TalkProTab: View {
|
||||
isOn.wrappedValue.toggle()
|
||||
} label: {
|
||||
Image(systemName: systemImage)
|
||||
.font(.system(size: 17, weight: .semibold))
|
||||
.font(OpenClawType.headline)
|
||||
.contentTransition(.symbolEffect(.replace))
|
||||
.frame(width: 44, height: 44)
|
||||
}
|
||||
@@ -518,7 +518,7 @@ private struct TalkProOrb: View {
|
||||
.frame(width: 92, height: 44)
|
||||
.opacity(self.showsWaveform ? 1 : 0)
|
||||
Image(systemName: self.systemImage)
|
||||
.font(.system(size: 34, weight: .bold))
|
||||
.font(OpenClawType.title1)
|
||||
.foregroundStyle(self.color)
|
||||
.opacity(self.showsWaveform ? 0.20 : 1)
|
||||
}
|
||||
|
||||
@@ -43,12 +43,12 @@ struct TalkRuntimeIssueDetailsSheet: View {
|
||||
Section {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text(self.issue.fallbackBannerTitle)
|
||||
.font(.title3.weight(.semibold))
|
||||
.font(OpenClawType.title3)
|
||||
Text(self.issue.fallbackBannerMessage)
|
||||
.font(.body)
|
||||
.font(OpenClawType.body)
|
||||
.foregroundStyle(.secondary)
|
||||
Text(self.issue.displayMessage)
|
||||
.font(.footnote.weight(.semibold))
|
||||
.font(OpenClawType.footnoteSemiBold)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
@@ -57,7 +57,7 @@ struct TalkRuntimeIssueDetailsSheet: View {
|
||||
|
||||
Section("Technical details") {
|
||||
Text(verbatim: self.issue.technicalDetails)
|
||||
.font(.system(.footnote, design: .monospaced))
|
||||
.font(OpenClawType.monoFootnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.textSelection(.enabled)
|
||||
Button("Copy diagnostics") {
|
||||
@@ -69,7 +69,7 @@ struct TalkRuntimeIssueDetailsSheet: View {
|
||||
if let copyFeedback {
|
||||
Section {
|
||||
Text(copyFeedback)
|
||||
.font(.footnote)
|
||||
.font(OpenClawType.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,117 @@
|
||||
Font Licenses and Provenance
|
||||
=============================
|
||||
|
||||
All bundled fonts are licensed under the SIL Open Font License, Version 1.1.
|
||||
The OFL permits bundling in software provided the copyright notice and license
|
||||
accompany the distribution.
|
||||
|
||||
Plus Jakarta Sans
|
||||
-----------------
|
||||
Copyright 2020 The Plus Jakarta Sans Project Authors
|
||||
(https://github.com/tokotype/PlusJakartaSans)
|
||||
License: SIL Open Font License 1.1
|
||||
Source: https://github.com/tokotype/PlusJakartaSans
|
||||
Version: 2.7.1
|
||||
|
||||
DM Sans
|
||||
-------
|
||||
Copyright 2014-2022 Indian Type Foundry
|
||||
Copyright 2014-2022 Jonny Pinhorn
|
||||
(https://github.com/googlefonts/dm-fonts)
|
||||
License: SIL Open Font License 1.1
|
||||
Source: https://github.com/googlefonts/dm-fonts
|
||||
|
||||
JetBrains Mono
|
||||
--------------
|
||||
Copyright 2020 The JetBrains Mono Project Authors
|
||||
(https://github.com/JetBrains/JetBrainsMono)
|
||||
License: SIL Open Font License 1.1
|
||||
Source: https://github.com/JetBrains/JetBrainsMono
|
||||
Version: 2.304
|
||||
|
||||
---
|
||||
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -60,17 +60,19 @@ private struct ExecApprovalPromptCard: View {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("Exec approval required")
|
||||
.font(.headline)
|
||||
.font(OpenClawType.headline)
|
||||
Text("Review this exec request before continuing. Your decision will be sent back to the gateway.")
|
||||
.font(.subheadline)
|
||||
.font(OpenClawType.subhead)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Text(self.prompt.commandText)
|
||||
.font(.system(size: 15, weight: .regular, design: .monospaced))
|
||||
.font(OpenClawType.mono)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(10)
|
||||
.background(.black.opacity(0.14), in: RoundedRectangle(cornerRadius: 12, style: .continuous))
|
||||
.background(
|
||||
.black.opacity(0.14),
|
||||
in: RoundedRectangle(cornerRadius: OpenClawRadius.md, style: .continuous))
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
if let host = self.normalized(self.prompt.host) {
|
||||
@@ -89,7 +91,7 @@ private struct ExecApprovalPromptCard: View {
|
||||
|
||||
if let errorText = self.normalized(self.errorText) {
|
||||
Text(errorText)
|
||||
.font(.footnote)
|
||||
.font(OpenClawType.footnote)
|
||||
.foregroundStyle(OpenClawBrand.danger)
|
||||
}
|
||||
|
||||
@@ -98,7 +100,7 @@ private struct ExecApprovalPromptCard: View {
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
Text("Resolving…")
|
||||
.font(.footnote)
|
||||
.font(OpenClawType.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
@@ -181,10 +183,10 @@ private struct ExecApprovalPromptMetadataRow: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(self.label)
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
Text(self.value)
|
||||
.font(.footnote)
|
||||
.font(OpenClawType.footnote)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,10 +19,10 @@ struct GatewayDiscoveryDebugLogView: View {
|
||||
ForEach(self.gatewayController.discoveryDebugLog) { entry in
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(Self.formatTime(entry.ts))
|
||||
.font(.caption)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
Text(entry.message)
|
||||
.font(.callout)
|
||||
.font(OpenClawType.callout)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
|
||||
@@ -89,12 +89,12 @@ struct GatewayProblemDetailsSheet: View {
|
||||
Section {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text(self.problem.title)
|
||||
.font(.title3.weight(.semibold))
|
||||
.font(OpenClawType.title3)
|
||||
Text(self.problem.message)
|
||||
.font(.body)
|
||||
.font(OpenClawType.body)
|
||||
.foregroundStyle(.secondary)
|
||||
Text(self.ownerSummary)
|
||||
.font(.footnote.weight(.semibold))
|
||||
.font(OpenClawType.footnoteSemiBold)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
@@ -104,7 +104,7 @@ struct GatewayProblemDetailsSheet: View {
|
||||
if let requestId = self.problem.requestId {
|
||||
Section("Request") {
|
||||
Text(verbatim: requestId)
|
||||
.font(.system(.body, design: .monospaced))
|
||||
.font(OpenClawType.mono)
|
||||
.textSelection(.enabled)
|
||||
Button("Copy request ID") {
|
||||
UIPasteboard.general.string = requestId
|
||||
@@ -116,7 +116,7 @@ struct GatewayProblemDetailsSheet: View {
|
||||
if let actionCommand = self.problem.actionCommand {
|
||||
Section("Gateway command") {
|
||||
Text(verbatim: actionCommand)
|
||||
.font(.system(.body, design: .monospaced))
|
||||
.font(OpenClawType.mono)
|
||||
.textSelection(.enabled)
|
||||
Button("Copy command") {
|
||||
UIPasteboard.general.string = actionCommand
|
||||
@@ -131,7 +131,7 @@ struct GatewayProblemDetailsSheet: View {
|
||||
Label("Open docs", systemImage: "book")
|
||||
}
|
||||
Text(verbatim: docsURL.absoluteString)
|
||||
.font(.footnote)
|
||||
.font(OpenClawType.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
@@ -140,7 +140,7 @@ struct GatewayProblemDetailsSheet: View {
|
||||
if let technicalDetails = self.problem.technicalDetails {
|
||||
Section("Technical details") {
|
||||
Text(verbatim: technicalDetails)
|
||||
.font(.system(.footnote, design: .monospaced))
|
||||
.font(OpenClawType.monoFootnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
@@ -149,7 +149,7 @@ struct GatewayProblemDetailsSheet: View {
|
||||
if let copyFeedback {
|
||||
Section {
|
||||
Text(copyFeedback)
|
||||
.font(.footnote)
|
||||
.font(OpenClawType.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ struct GatewayQuickSetupSheet: View {
|
||||
NavigationStack {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text("Connect to a Gateway?")
|
||||
.font(.title2.bold())
|
||||
.font(OpenClawType.title2)
|
||||
|
||||
if let gatewayProblem = self.appModel.lastGatewayProblem {
|
||||
GatewayProblemBanner(
|
||||
@@ -46,7 +46,6 @@ struct GatewayQuickSetupSheet: View {
|
||||
await MainActor.run {
|
||||
self.connecting = false
|
||||
self.connectError = err
|
||||
// If we kicked off a connect, leave the sheet up so the user can see status evolve.
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
@@ -60,14 +59,13 @@ struct GatewayQuickSetupSheet: View {
|
||||
Text("Connect")
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.openClawPrimaryButton()
|
||||
.disabled(self.connecting)
|
||||
|
||||
if let connectError {
|
||||
Text(connectError)
|
||||
.font(.footnote)
|
||||
.font(OpenClawType.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
@@ -76,9 +74,8 @@ struct GatewayQuickSetupSheet: View {
|
||||
self.dismiss()
|
||||
} label: {
|
||||
Text("Not now")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.openClawSecondaryButton()
|
||||
.disabled(self.connecting)
|
||||
|
||||
self.fullRowToggle("Don’t show this again", isOn: self.$quickSetupDismissed)
|
||||
@@ -173,10 +170,10 @@ private struct GatewayQuickSetupCandidatePanel: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(verbatim: self.name)
|
||||
.font(.system(.headline, design: .monospaced))
|
||||
.font(OpenClawType.monoHeadline)
|
||||
.foregroundStyle(.primary)
|
||||
Text(verbatim: self.debugID)
|
||||
.font(.system(.footnote, design: .monospaced))
|
||||
.font(OpenClawType.monoFootnote)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
@@ -187,7 +184,7 @@ private struct GatewayQuickSetupCandidatePanel: View {
|
||||
Text(verbatim: "Node: \(self.nodeStatusText)")
|
||||
Text(verbatim: "Operator: \(self.operatorStatusText)")
|
||||
}
|
||||
.font(.system(.footnote, design: .monospaced))
|
||||
.font(OpenClawType.monoFootnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.frame(maxWidth: Self.readableMonospaceWidth, alignment: .leading)
|
||||
@@ -195,7 +192,7 @@ private struct GatewayQuickSetupCandidatePanel: View {
|
||||
.padding(.horizontal, 16)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.textSelection(.enabled)
|
||||
.background(.thinMaterial)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 14))
|
||||
.background(OpenClawBrand.obsidian)
|
||||
.clipShape(RoundedRectangle(cornerRadius: OpenClawRadius.lg))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,14 +50,14 @@ private struct NotificationPermissionGuidanceCard: View {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("Notifications are off")
|
||||
.font(.headline)
|
||||
.font(OpenClawType.headline)
|
||||
Text(
|
||||
"""
|
||||
Exec approvals can only be reviewed while OpenClaw is open and connected.
|
||||
|
||||
Enable Notifications to receive approval notifications while OpenClaw is not open.
|
||||
""")
|
||||
.font(.subheadline)
|
||||
.font(OpenClawType.subhead)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
@@ -88,6 +88,23 @@
|
||||
<string>$(OPENCLAW_PUSH_MODE)</string>
|
||||
<key>OpenClawPushRelayBaseURL</key>
|
||||
<string>$(OPENCLAW_PUSH_RELAY_BASE_URL)</string>
|
||||
<key>UIAppFonts</key>
|
||||
<array>
|
||||
<string>PlusJakartaSans-Light.ttf</string>
|
||||
<string>PlusJakartaSans-Regular.ttf</string>
|
||||
<string>PlusJakartaSans-Medium.ttf</string>
|
||||
<string>PlusJakartaSans-SemiBold.ttf</string>
|
||||
<string>PlusJakartaSans-Bold.ttf</string>
|
||||
<string>PlusJakartaSans-ExtraBold.ttf</string>
|
||||
<string>DMSans-Light.ttf</string>
|
||||
<string>DMSans-Regular.ttf</string>
|
||||
<string>DMSans-Italic.ttf</string>
|
||||
<string>DMSans-Medium.ttf</string>
|
||||
<string>DMSans-SemiBold.ttf</string>
|
||||
<string>JetBrainsMono-Regular.ttf</string>
|
||||
<string>JetBrainsMono-Medium.ttf</string>
|
||||
<string>JetBrainsMono-SemiBold.ttf</string>
|
||||
</array>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
|
||||
@@ -11,15 +11,16 @@ struct OnboardingIntroStep: View {
|
||||
.padding(.bottom, 18)
|
||||
|
||||
Text("Welcome to OpenClaw")
|
||||
.font(.largeTitle.weight(.bold))
|
||||
.font(OpenClawType.title1)
|
||||
.foregroundStyle(OpenClawBrand.textPrimary)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.bottom, 10)
|
||||
|
||||
Text("Turn this device into a secure OpenClaw node for chat, voice, camera, and device tools.")
|
||||
.font(.subheadline)
|
||||
.font(OpenClawType.subhead)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 32)
|
||||
.padding(.horizontal, OpenClawSpacing.space8)
|
||||
.padding(.bottom, 24)
|
||||
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
@@ -27,42 +28,43 @@ struct OnboardingIntroStep: View {
|
||||
Label("Choose device permissions", systemImage: "hand.raised")
|
||||
Label("Use OpenClaw from your phone", systemImage: "message.fill")
|
||||
}
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(18)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 20, style: .continuous)
|
||||
.fill(Color(uiColor: .secondarySystemBackground))
|
||||
RoundedRectangle(cornerRadius: OpenClawRadius.xl, style: .continuous)
|
||||
.fill(OpenClawBrand.slate)
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.horizontal, OpenClawSpacing.space6)
|
||||
.padding(.bottom, 16)
|
||||
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.font(.title3.weight(.semibold))
|
||||
.font(OpenClawType.title3)
|
||||
.foregroundStyle(OpenClawBrand.warn)
|
||||
.frame(width: 24)
|
||||
.padding(.top, 2)
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("Security notice")
|
||||
.font(.headline)
|
||||
.font(OpenClawType.headline)
|
||||
.foregroundStyle(OpenClawBrand.textPrimary)
|
||||
Text(
|
||||
"The connected OpenClaw agent can use device capabilities you enable, "
|
||||
+ "such as camera, microphone, photos, contacts, calendar, and location. "
|
||||
+ "Continue only if you trust the gateway and agent you connect to.")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.font(OpenClawType.footnote)
|
||||
.foregroundStyle(OpenClawBrand.textSecondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(18)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 20, style: .continuous)
|
||||
.fill(Color(uiColor: .secondarySystemBackground))
|
||||
RoundedRectangle(cornerRadius: OpenClawRadius.xl, style: .continuous)
|
||||
.fill(OpenClawBrand.slate)
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.horizontal, OpenClawSpacing.space6)
|
||||
|
||||
Spacer()
|
||||
|
||||
@@ -70,11 +72,9 @@ struct OnboardingIntroStep: View {
|
||||
self.onContinue()
|
||||
} label: {
|
||||
Text("Continue")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.controlSize(.large)
|
||||
.padding(.horizontal, 24)
|
||||
.openClawPrimaryButton()
|
||||
.padding(.horizontal, OpenClawSpacing.space6)
|
||||
.padding(.bottom, 48)
|
||||
}
|
||||
}
|
||||
@@ -95,34 +95,35 @@ struct OnboardingWelcomeStep: View {
|
||||
.padding(.bottom, 20)
|
||||
|
||||
Text("Connect Gateway")
|
||||
.font(.largeTitle.weight(.bold))
|
||||
.font(OpenClawType.title1)
|
||||
.foregroundStyle(OpenClawBrand.textPrimary)
|
||||
.padding(.bottom, 8)
|
||||
|
||||
Text("Scan a QR code from your OpenClaw gateway or continue with manual setup.")
|
||||
.font(.subheadline)
|
||||
.font(OpenClawType.subhead)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 32)
|
||||
.padding(.horizontal, OpenClawSpacing.space8)
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("How to pair")
|
||||
.font(.headline)
|
||||
.font(OpenClawType.headline)
|
||||
Text("In your OpenClaw chat, run")
|
||||
.font(.footnote)
|
||||
.font(OpenClawType.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
Text("/pair qr")
|
||||
.font(.system(.footnote, design: .monospaced).weight(.semibold))
|
||||
.font(OpenClawType.monoFootnote)
|
||||
Text("Then scan the QR code here to connect this device.")
|
||||
.font(.footnote)
|
||||
.font(OpenClawType.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(16)
|
||||
.padding(OpenClawSpacing.space4)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 18, style: .continuous)
|
||||
.fill(Color(uiColor: .secondarySystemBackground))
|
||||
RoundedRectangle(cornerRadius: OpenClawRadius.xl, style: .continuous)
|
||||
.fill(OpenClawBrand.slate)
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.horizontal, OpenClawSpacing.space6)
|
||||
.padding(.top, 20)
|
||||
|
||||
Spacer()
|
||||
@@ -132,28 +133,24 @@ struct OnboardingWelcomeStep: View {
|
||||
self.onScanQRCode()
|
||||
} label: {
|
||||
Label("Scan QR Code", systemImage: "qrcode")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.controlSize(.large)
|
||||
.openClawPrimaryButton()
|
||||
|
||||
Button {
|
||||
self.onManualSetup()
|
||||
} label: {
|
||||
Text("Set Up Manually")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.controlSize(.large)
|
||||
.openClawSecondaryButton()
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.horizontal, OpenClawSpacing.space6)
|
||||
.padding(.bottom, 12)
|
||||
|
||||
Text(self.statusLine)
|
||||
.font(.footnote)
|
||||
.font(OpenClawType.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.horizontal, OpenClawSpacing.space6)
|
||||
.padding(.bottom, 48)
|
||||
}
|
||||
}
|
||||
@@ -170,9 +167,9 @@ struct OnboardingModeRow: View {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(self.title)
|
||||
.font(.body.weight(.semibold))
|
||||
.font(OpenClawType.headline)
|
||||
Text(self.subtitle)
|
||||
.font(.footnote)
|
||||
.font(OpenClawType.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
|
||||
@@ -129,9 +129,9 @@ struct OnboardingWizardView: View {
|
||||
ToolbarItem(placement: .principal) {
|
||||
VStack(spacing: 2) {
|
||||
Text(self.step.title)
|
||||
.font(.headline)
|
||||
.font(OpenClawType.headline)
|
||||
Text(self.step.manualProgressTitle)
|
||||
.font(.caption2)
|
||||
.font(OpenClawType.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
@@ -374,7 +374,7 @@ struct OnboardingWizardView: View {
|
||||
HStack {
|
||||
Text(title)
|
||||
Spacer(minLength: 8)
|
||||
self.onboardingSwitchIndicator(isOn: isOn.wrappedValue)
|
||||
OpenClawToggleIndicator(isOn: isOn.wrappedValue)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
@@ -383,19 +383,6 @@ struct OnboardingWizardView: View {
|
||||
.accessibilityValue(isOn.wrappedValue ? "On" : "Off")
|
||||
}
|
||||
|
||||
private func onboardingSwitchIndicator(isOn: Bool) -> some View {
|
||||
Capsule()
|
||||
.fill(isOn ? OpenClawBrand.accent : Color.secondary.opacity(0.35))
|
||||
.frame(width: 52, height: 32)
|
||||
.overlay(alignment: isOn ? .trailing : .leading) {
|
||||
Circle()
|
||||
.fill(Color.white)
|
||||
.frame(width: 28, height: 28)
|
||||
.padding(2)
|
||||
.shadow(color: Color.black.opacity(0.14), radius: 1, x: 0, y: 1)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var connectStep: some View {
|
||||
if let selectedMode {
|
||||
@@ -445,7 +432,7 @@ struct OnboardingWizardView: View {
|
||||
Text(gateway.name)
|
||||
if let host = gateway.lanHost ?? gateway.tailnetDns {
|
||||
Text(host)
|
||||
.font(.footnote)
|
||||
.font(OpenClawType.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
@@ -517,11 +504,11 @@ struct OnboardingWizardView: View {
|
||||
})
|
||||
} else if self.issue.needsAuthToken {
|
||||
Text("Gateway rejected credentials. Scan a fresh QR code or update token/password.")
|
||||
.font(.footnote)
|
||||
.font(OpenClawType.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
} else {
|
||||
Text("Auth token looks valid.")
|
||||
.font(.footnote)
|
||||
.font(OpenClawType.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
@@ -586,18 +573,19 @@ struct OnboardingWizardView: View {
|
||||
.padding(.bottom, 20)
|
||||
|
||||
Text("Connected")
|
||||
.font(.largeTitle.weight(.bold))
|
||||
.font(OpenClawType.title1)
|
||||
.foregroundStyle(OpenClawBrand.textPrimary)
|
||||
.padding(.bottom, 8)
|
||||
|
||||
let server = self.appModel.gatewayServerName ?? "gateway"
|
||||
Text(server)
|
||||
.font(.subheadline)
|
||||
.font(OpenClawType.subhead)
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom, 4)
|
||||
|
||||
if let addr = self.appModel.gatewayRemoteAddress {
|
||||
Text(addr)
|
||||
.font(.subheadline)
|
||||
.font(OpenClawType.subhead)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
@@ -607,11 +595,9 @@ struct OnboardingWizardView: View {
|
||||
self.onClose()
|
||||
} label: {
|
||||
Text("Open OpenClaw")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.controlSize(.large)
|
||||
.padding(.horizontal, 24)
|
||||
.openClawPrimaryButton()
|
||||
.padding(.horizontal, OpenClawSpacing.space6)
|
||||
.padding(.bottom, 48)
|
||||
}
|
||||
}
|
||||
@@ -646,7 +632,7 @@ extension OnboardingWizardView {
|
||||
|
||||
if let setupCodeStatus, !setupCodeStatus.isEmpty {
|
||||
Text(setupCodeStatus)
|
||||
.font(.footnote)
|
||||
.font(OpenClawType.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
} header: {
|
||||
|
||||
@@ -75,17 +75,15 @@ struct PrivacyAccessSectionView: View {
|
||||
HStack {
|
||||
Label(title, systemImage: icon)
|
||||
Spacer()
|
||||
Text(status)
|
||||
.font(.footnote.weight(.medium))
|
||||
.foregroundStyle(self.statusColor(for: status))
|
||||
OpenClawStatusBadge(label: status, tone: self.statusTone(for: status))
|
||||
.accessibilityIdentifier("privacy-access-\(title)-status")
|
||||
}
|
||||
Text(detail)
|
||||
.font(.footnote)
|
||||
.font(OpenClawType.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
if let actionTitle, let action {
|
||||
Button(actionTitle, action: action)
|
||||
.font(.footnote)
|
||||
.font(OpenClawType.footnote)
|
||||
.buttonStyle(.bordered)
|
||||
.accessibilityIdentifier("privacy-access-\(title)-action")
|
||||
}
|
||||
@@ -93,16 +91,16 @@ struct PrivacyAccessSectionView: View {
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
|
||||
private func statusColor(for status: String) -> Color {
|
||||
private func statusTone(for status: String) -> OpenClawStatusTone {
|
||||
switch status {
|
||||
case "Allowed", "Limited":
|
||||
OpenClawBrand.ok
|
||||
.ok
|
||||
case "Not Set":
|
||||
OpenClawBrand.warn
|
||||
.warn
|
||||
case "Add-Only":
|
||||
OpenClawBrand.warn
|
||||
.warn
|
||||
default:
|
||||
OpenClawBrand.danger
|
||||
.danger
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,11 +6,11 @@ struct VoiceWakeToast: View {
|
||||
var body: some View {
|
||||
HStack(spacing: 10) {
|
||||
Image(systemName: "mic.fill")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.foregroundStyle(.primary)
|
||||
|
||||
Text(self.command)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.foregroundStyle(.primary)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
|
||||
@@ -34,7 +34,7 @@ struct TalkPermissionPromptView: View {
|
||||
VStack(alignment: .leading, spacing: self.style == .sheet ? 16 : 12) {
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
Image(systemName: self.iconSystemName)
|
||||
.font(.title3.weight(.semibold))
|
||||
.font(OpenClawType.title3)
|
||||
.foregroundStyle(self.requestIsPending ? OpenClawBrand.warn : OpenClawBrand.accent)
|
||||
.frame(width: 28, height: 28)
|
||||
|
||||
@@ -42,7 +42,7 @@ struct TalkPermissionPromptView: View {
|
||||
Text(self.titleText)
|
||||
.font(self.style == .sheet ? .title3.weight(.semibold) : .headline)
|
||||
Text(self.messageText)
|
||||
.font(.footnote)
|
||||
.font(OpenClawType.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
@@ -50,7 +50,7 @@ struct TalkPermissionPromptView: View {
|
||||
|
||||
if let failureMessage = self.state.failureMessage {
|
||||
Label(failureMessage, systemImage: "exclamationmark.triangle.fill")
|
||||
.font(.footnote)
|
||||
.font(OpenClawType.footnote)
|
||||
.foregroundStyle(OpenClawBrand.danger)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
@@ -58,7 +58,7 @@ struct TalkPermissionPromptView: View {
|
||||
if let requestId = self.state.requestId {
|
||||
LabeledContent("Request ID") {
|
||||
Text(requestId)
|
||||
.font(.caption.monospaced())
|
||||
.font(OpenClawType.monoSmall)
|
||||
.foregroundStyle(.secondary)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
@@ -92,13 +92,13 @@ struct TalkPermissionPromptView: View {
|
||||
.padding(self.style == .card || self.style == .sheet ? 16 : 0)
|
||||
.background {
|
||||
if self.style == .card || self.style == .sheet {
|
||||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||
RoundedRectangle(cornerRadius: OpenClawRadius.md, style: .continuous)
|
||||
.fill(.thinMaterial)
|
||||
}
|
||||
}
|
||||
.overlay {
|
||||
if self.style == .card || self.style == .sheet {
|
||||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||
RoundedRectangle(cornerRadius: OpenClawRadius.md, style: .continuous)
|
||||
.stroke(OpenClawBrand.accent.opacity(0.20), lineWidth: 1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import Testing
|
||||
import UIKit
|
||||
@testable import OpenClaw
|
||||
|
||||
struct OpenClawTypographyTests {
|
||||
@Test func `bundled fonts load from app bundle`() {
|
||||
for name in OpenClawType.registeredPostScriptNames {
|
||||
#expect(UIFont(name: name, size: 12) != nil, "Missing bundled font: \(name)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func `dynamic type scales display fonts`() {
|
||||
guard let base = UIFont(name: "PlusJakartaSans-ExtraBold", size: 34) else {
|
||||
Issue.record("PlusJakartaSans-ExtraBold should be bundled")
|
||||
return
|
||||
}
|
||||
|
||||
let defaultTraits = UITraitCollection(preferredContentSizeCategory: .large)
|
||||
let largeTraits = UITraitCollection(preferredContentSizeCategory: .accessibilityExtraExtraExtraLarge)
|
||||
let metrics = UIFontMetrics(forTextStyle: .largeTitle)
|
||||
|
||||
let defaultSize = metrics.scaledFont(for: base, compatibleWith: defaultTraits).pointSize
|
||||
let largeSize = metrics.scaledFont(for: base, compatibleWith: largeTraits).pointSize
|
||||
|
||||
#expect(largeSize > defaultSize)
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,8 @@ targets:
|
||||
- path: Resources/Licenses
|
||||
type: folder
|
||||
buildPhase: resources
|
||||
- path: Sources/Fonts
|
||||
buildPhase: resources
|
||||
resources:
|
||||
- path: Resources/Localizable.xcstrings
|
||||
dependencies:
|
||||
@@ -177,6 +179,21 @@ targets:
|
||||
NSSpeechRecognitionUsageDescription: OpenClaw uses on-device speech recognition for talk mode and voice wake.
|
||||
NSSupportsLiveActivities: true
|
||||
ITSAppUsesNonExemptEncryption: false
|
||||
UIAppFonts:
|
||||
- PlusJakartaSans-Light.ttf
|
||||
- PlusJakartaSans-Regular.ttf
|
||||
- PlusJakartaSans-Medium.ttf
|
||||
- PlusJakartaSans-SemiBold.ttf
|
||||
- PlusJakartaSans-Bold.ttf
|
||||
- PlusJakartaSans-ExtraBold.ttf
|
||||
- DMSans-Light.ttf
|
||||
- DMSans-Regular.ttf
|
||||
- DMSans-Italic.ttf
|
||||
- DMSans-Medium.ttf
|
||||
- DMSans-SemiBold.ttf
|
||||
- JetBrainsMono-Regular.ttf
|
||||
- JetBrainsMono-Medium.ttf
|
||||
- JetBrainsMono-SemiBold.ttf
|
||||
OpenClawPushMode: "$(OPENCLAW_PUSH_MODE)"
|
||||
OpenClawPushRelayBaseURL: "$(OPENCLAW_PUSH_RELAY_BASE_URL)"
|
||||
UISupportedInterfaceOrientations:
|
||||
|
||||
Reference in New Issue
Block a user