fix(mac): sessions error UI + sleeping icon
parent
a11a204b8e
commit
9d47b15575
|
|
@ -3,6 +3,7 @@ import SwiftUI
|
||||||
|
|
||||||
struct CritterStatusLabel: View {
|
struct CritterStatusLabel: View {
|
||||||
var isPaused: Bool
|
var isPaused: Bool
|
||||||
|
var isSleeping: Bool
|
||||||
var isWorking: Bool
|
var isWorking: Bool
|
||||||
var earBoostActive: Bool
|
var earBoostActive: Bool
|
||||||
var blinkTick: Int
|
var blinkTick: Int
|
||||||
|
|
@ -25,6 +26,10 @@ struct CritterStatusLabel: View {
|
||||||
self.iconState.isWorking || self.isWorking
|
self.iconState.isWorking || self.isWorking
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var effectiveAnimationsEnabled: Bool {
|
||||||
|
self.animationsEnabled && !self.isSleeping
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack(alignment: .topTrailing) {
|
ZStack(alignment: .topTrailing) {
|
||||||
self.iconImage
|
self.iconImage
|
||||||
|
|
@ -34,7 +39,7 @@ struct CritterStatusLabel: View {
|
||||||
// Avoid Combine's TimerPublisher here: on macOS 26.2 we've seen crashes inside executor checks
|
// Avoid Combine's TimerPublisher here: on macOS 26.2 we've seen crashes inside executor checks
|
||||||
// triggered by its callbacks. Drive periodic updates via a Swift-concurrency task instead.
|
// triggered by its callbacks. Drive periodic updates via a Swift-concurrency task instead.
|
||||||
.task(id: self.tickTaskID) {
|
.task(id: self.tickTaskID) {
|
||||||
guard self.animationsEnabled, !self.earBoostActive else {
|
guard self.effectiveAnimationsEnabled, !self.earBoostActive else {
|
||||||
await MainActor.run { self.resetMotion() }
|
await MainActor.run { self.resetMotion() }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -47,24 +52,27 @@ struct CritterStatusLabel: View {
|
||||||
}
|
}
|
||||||
.onChange(of: self.isPaused) { _, _ in self.resetMotion() }
|
.onChange(of: self.isPaused) { _, _ in self.resetMotion() }
|
||||||
.onChange(of: self.blinkTick) { _, _ in
|
.onChange(of: self.blinkTick) { _, _ in
|
||||||
guard !self.earBoostActive else { return }
|
guard self.effectiveAnimationsEnabled, !self.earBoostActive else { return }
|
||||||
self.blink()
|
self.blink()
|
||||||
}
|
}
|
||||||
.onChange(of: self.sendCelebrationTick) { _, _ in
|
.onChange(of: self.sendCelebrationTick) { _, _ in
|
||||||
guard !self.earBoostActive else { return }
|
guard self.effectiveAnimationsEnabled, !self.earBoostActive else { return }
|
||||||
self.wiggleLegs()
|
self.wiggleLegs()
|
||||||
}
|
}
|
||||||
.onChange(of: self.animationsEnabled) { _, enabled in
|
.onChange(of: self.animationsEnabled) { _, enabled in
|
||||||
if enabled {
|
if enabled, !self.isSleeping {
|
||||||
self.scheduleRandomTimers(from: Date())
|
self.scheduleRandomTimers(from: Date())
|
||||||
} else {
|
} else {
|
||||||
self.resetMotion()
|
self.resetMotion()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onChange(of: self.isSleeping) { _, _ in
|
||||||
|
self.resetMotion()
|
||||||
|
}
|
||||||
.onChange(of: self.earBoostActive) { _, active in
|
.onChange(of: self.earBoostActive) { _, active in
|
||||||
if active {
|
if active {
|
||||||
self.resetMotion()
|
self.resetMotion()
|
||||||
} else if self.animationsEnabled {
|
} else if self.effectiveAnimationsEnabled {
|
||||||
self.scheduleRandomTimers(from: Date())
|
self.scheduleRandomTimers(from: Date())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -81,11 +89,11 @@ struct CritterStatusLabel: View {
|
||||||
|
|
||||||
private var tickTaskID: Int {
|
private var tickTaskID: Int {
|
||||||
// Ensure SwiftUI restarts (and cancels) the task when these change.
|
// Ensure SwiftUI restarts (and cancels) the task when these change.
|
||||||
(self.animationsEnabled ? 1 : 0) | (self.earBoostActive ? 2 : 0)
|
(self.effectiveAnimationsEnabled ? 1 : 0) | (self.earBoostActive ? 2 : 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func tick(_ now: Date) {
|
private func tick(_ now: Date) {
|
||||||
guard self.animationsEnabled, !self.earBoostActive else {
|
guard self.effectiveAnimationsEnabled, !self.earBoostActive else {
|
||||||
self.resetMotion()
|
self.resetMotion()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -128,6 +136,10 @@ struct CritterStatusLabel: View {
|
||||||
return Image(nsImage: CritterIconRenderer.makeIcon(blink: 0, badge: nil))
|
return Image(nsImage: CritterIconRenderer.makeIcon(blink: 0, badge: nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.isSleeping {
|
||||||
|
return Image(nsImage: CritterIconRenderer.makeIcon(blink: 1, badge: nil))
|
||||||
|
}
|
||||||
|
|
||||||
return Image(nsImage: CritterIconRenderer.makeIcon(
|
return Image(nsImage: CritterIconRenderer.makeIcon(
|
||||||
blink: self.blinkAmount,
|
blink: self.blinkAmount,
|
||||||
legWiggle: max(self.legWiggle, self.isWorkingNow ? 0.6 : 0),
|
legWiggle: max(self.legWiggle, self.isWorkingNow ? 0.6 : 0),
|
||||||
|
|
@ -216,11 +228,12 @@ struct CritterStatusLabel: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private var gatewayNeedsAttention: Bool {
|
private var gatewayNeedsAttention: Bool {
|
||||||
|
if self.isSleeping { return false }
|
||||||
switch self.gatewayStatus {
|
switch self.gatewayStatus {
|
||||||
case .failed, .stopped:
|
case .failed, .stopped:
|
||||||
!self.isPaused
|
return !self.isPaused
|
||||||
case .starting, .running, .attachedExisting:
|
case .starting, .running, .attachedExisting:
|
||||||
false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ struct ClawdisApp: App {
|
||||||
@NSApplicationDelegateAdaptor(AppDelegate.self) private var delegate
|
@NSApplicationDelegateAdaptor(AppDelegate.self) private var delegate
|
||||||
@State private var state: AppState
|
@State private var state: AppState
|
||||||
private let gatewayManager = GatewayProcessManager.shared
|
private let gatewayManager = GatewayProcessManager.shared
|
||||||
|
private let controlChannel = ControlChannel.shared
|
||||||
private let activityStore = WorkActivityStore.shared
|
private let activityStore = WorkActivityStore.shared
|
||||||
@State private var statusItem: NSStatusItem?
|
@State private var statusItem: NSStatusItem?
|
||||||
@State private var isMenuPresented = false
|
@State private var isMenuPresented = false
|
||||||
|
|
@ -35,29 +36,36 @@ struct ClawdisApp: App {
|
||||||
MenuBarExtra { MenuContent(state: self.state, updater: self.delegate.updaterController) } label: {
|
MenuBarExtra { MenuContent(state: self.state, updater: self.delegate.updaterController) } label: {
|
||||||
CritterStatusLabel(
|
CritterStatusLabel(
|
||||||
isPaused: self.state.isPaused,
|
isPaused: self.state.isPaused,
|
||||||
|
isSleeping: self.isGatewaySleeping,
|
||||||
isWorking: self.state.isWorking,
|
isWorking: self.state.isWorking,
|
||||||
earBoostActive: self.state.earBoostActive,
|
earBoostActive: self.state.earBoostActive,
|
||||||
blinkTick: self.state.blinkTick,
|
blinkTick: self.state.blinkTick,
|
||||||
sendCelebrationTick: self.state.sendCelebrationTick,
|
sendCelebrationTick: self.state.sendCelebrationTick,
|
||||||
gatewayStatus: self.gatewayManager.status,
|
gatewayStatus: self.gatewayManager.status,
|
||||||
animationsEnabled: self.state.iconAnimationsEnabled,
|
animationsEnabled: self.state.iconAnimationsEnabled && !self.isGatewaySleeping,
|
||||||
iconState: self.effectiveIconState)
|
iconState: self.effectiveIconState)
|
||||||
}
|
}
|
||||||
.menuBarExtraStyle(.menu)
|
.menuBarExtraStyle(.menu)
|
||||||
.menuBarExtraAccess(isPresented: self.$isMenuPresented) { item in
|
.menuBarExtraAccess(isPresented: self.$isMenuPresented) { item in
|
||||||
self.statusItem = item
|
self.statusItem = item
|
||||||
self.applyStatusItemAppearance(paused: self.state.isPaused)
|
self.applyStatusItemAppearance(paused: self.state.isPaused, sleeping: self.isGatewaySleeping)
|
||||||
self.installStatusItemMouseHandler(for: item)
|
self.installStatusItemMouseHandler(for: item)
|
||||||
self.updateHoverHUDSuppression()
|
self.updateHoverHUDSuppression()
|
||||||
}
|
}
|
||||||
.onChange(of: self.state.isPaused) { _, paused in
|
.onChange(of: self.state.isPaused) { _, paused in
|
||||||
self.applyStatusItemAppearance(paused: paused)
|
self.applyStatusItemAppearance(paused: paused, sleeping: self.isGatewaySleeping)
|
||||||
if self.state.connectionMode == .local {
|
if self.state.connectionMode == .local {
|
||||||
self.gatewayManager.setActive(!paused)
|
self.gatewayManager.setActive(!paused)
|
||||||
} else {
|
} else {
|
||||||
self.gatewayManager.stop()
|
self.gatewayManager.stop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onChange(of: self.controlChannel.state) { _, _ in
|
||||||
|
self.applyStatusItemAppearance(paused: self.state.isPaused, sleeping: self.isGatewaySleeping)
|
||||||
|
}
|
||||||
|
.onChange(of: self.gatewayManager.status) { _, _ in
|
||||||
|
self.applyStatusItemAppearance(paused: self.state.isPaused, sleeping: self.isGatewaySleeping)
|
||||||
|
}
|
||||||
.onChange(of: self.state.connectionMode) { _, mode in
|
.onChange(of: self.state.connectionMode) { _, mode in
|
||||||
Task { await ConnectionModeCoordinator.shared.apply(mode: mode, paused: self.state.isPaused) }
|
Task { await ConnectionModeCoordinator.shared.apply(mode: mode, paused: self.state.isPaused) }
|
||||||
}
|
}
|
||||||
|
|
@ -75,8 +83,27 @@ struct ClawdisApp: App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func applyStatusItemAppearance(paused: Bool) {
|
private func applyStatusItemAppearance(paused: Bool, sleeping: Bool) {
|
||||||
self.statusItem?.button?.appearsDisabled = paused
|
self.statusItem?.button?.appearsDisabled = paused || sleeping
|
||||||
|
}
|
||||||
|
|
||||||
|
private var isGatewaySleeping: Bool {
|
||||||
|
if self.state.isPaused { return false }
|
||||||
|
switch self.state.connectionMode {
|
||||||
|
case .unconfigured:
|
||||||
|
return true
|
||||||
|
case .remote:
|
||||||
|
if case .connected = self.controlChannel.state { return false }
|
||||||
|
return true
|
||||||
|
case .local:
|
||||||
|
switch self.gatewayManager.status {
|
||||||
|
case .running, .starting, .attachedExisting:
|
||||||
|
if case .connected = self.controlChannel.state { return false }
|
||||||
|
return true
|
||||||
|
case .failed, .stopped:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ struct MenuContent: View {
|
||||||
@State private var loadingMics = false
|
@State private var loadingMics = false
|
||||||
@State private var sessionMenu: [SessionRow] = []
|
@State private var sessionMenu: [SessionRow] = []
|
||||||
@State private var sessionStorePath: String?
|
@State private var sessionStorePath: String?
|
||||||
|
@State private var sessionLoading = true
|
||||||
|
@State private var sessionErrorText: String?
|
||||||
@State private var browserControlEnabled = true
|
@State private var browserControlEnabled = true
|
||||||
private let sessionMenuItemWidth: CGFloat = 320
|
private let sessionMenuItemWidth: CGFloat = 320
|
||||||
private let sessionMenuActiveWindowSeconds: TimeInterval = 24 * 60 * 60
|
private let sessionMenuActiveWindowSeconds: TimeInterval = 24 * 60 * 60
|
||||||
|
|
@ -31,7 +33,9 @@ struct MenuContent: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.disabled(self.state.connectionMode == .unconfigured)
|
.disabled(self.state.connectionMode == .unconfigured)
|
||||||
|
|
||||||
self.sessionsSection
|
self.sessionsSection
|
||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
Toggle(isOn: self.heartbeatsBinding) {
|
Toggle(isOn: self.heartbeatsBinding) {
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
|
@ -196,12 +200,39 @@ struct MenuContent: View {
|
||||||
|
|
||||||
private var sessionsSection: some View {
|
private var sessionsSection: some View {
|
||||||
Group {
|
Group {
|
||||||
Divider()
|
MenuHostedItem(
|
||||||
|
width: self.sessionMenuItemWidth,
|
||||||
|
rootView: AnyView(MenuSessionsHeaderView(
|
||||||
|
count: self.sessionMenu.count,
|
||||||
|
statusText: self.sessionLoading
|
||||||
|
? "Loading sessions…"
|
||||||
|
: (self.sessionMenu.isEmpty ? nil : self.sessionErrorText))))
|
||||||
|
.disabled(true)
|
||||||
|
|
||||||
if self.sessionMenu.isEmpty {
|
if self.sessionMenu.isEmpty, !self.sessionLoading, let error = self.sessionErrorText, !error.isEmpty {
|
||||||
Text("No active sessions")
|
MenuHostedItem(
|
||||||
.font(.caption)
|
width: self.sessionMenuItemWidth,
|
||||||
.foregroundStyle(.secondary)
|
rootView: AnyView(
|
||||||
|
Label(error, systemImage: "exclamationmark.triangle")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
.truncationMode(.tail)
|
||||||
|
.padding(.leading, 20)
|
||||||
|
.padding(.trailing, 10)
|
||||||
|
.padding(.vertical, 6)
|
||||||
|
.frame(minWidth: 300, alignment: .leading)))
|
||||||
|
.disabled(true)
|
||||||
|
} else if self.sessionMenu.isEmpty, !self.sessionLoading, self.sessionErrorText == nil {
|
||||||
|
MenuHostedItem(
|
||||||
|
width: self.sessionMenuItemWidth,
|
||||||
|
rootView: AnyView(Text("No active sessions")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.padding(.leading, 20)
|
||||||
|
.padding(.trailing, 10)
|
||||||
|
.padding(.vertical, 6)
|
||||||
|
.frame(minWidth: 300, alignment: .leading)))
|
||||||
.disabled(true)
|
.disabled(true)
|
||||||
} else {
|
} else {
|
||||||
ForEach(self.sessionMenu) { row in
|
ForEach(self.sessionMenu) { row in
|
||||||
|
|
@ -375,6 +406,45 @@ struct MenuContent: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private func reloadSessionMenu() async {
|
||||||
|
self.sessionLoading = true
|
||||||
|
self.sessionErrorText = nil
|
||||||
|
|
||||||
|
do {
|
||||||
|
let snapshot = try await SessionLoader.loadSnapshot(limit: 32)
|
||||||
|
self.sessionStorePath = snapshot.storePath
|
||||||
|
let now = Date()
|
||||||
|
let active = snapshot.rows.filter { row in
|
||||||
|
if row.key == "main" { return true }
|
||||||
|
guard let updatedAt = row.updatedAt else { return false }
|
||||||
|
return now.timeIntervalSince(updatedAt) <= self.sessionMenuActiveWindowSeconds
|
||||||
|
}
|
||||||
|
self.sessionMenu = active.sorted { lhs, rhs in
|
||||||
|
if lhs.key == "main" { return true }
|
||||||
|
if rhs.key == "main" { return false }
|
||||||
|
return (lhs.updatedAt ?? .distantPast) > (rhs.updatedAt ?? .distantPast)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Keep the previous snapshot (if any) so the menu doesn't go empty while the gateway is flaky.
|
||||||
|
self.sessionErrorText = self.compactSessionError(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.sessionLoading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private func compactSessionError(_ error: Error) -> String {
|
||||||
|
if let loadError = error as? SessionLoadError {
|
||||||
|
switch loadError {
|
||||||
|
case .gatewayUnavailable:
|
||||||
|
return "Sessions unavailable — gateway unreachable"
|
||||||
|
case .decodeFailed:
|
||||||
|
return "Sessions unavailable — invalid payload"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "Sessions unavailable"
|
||||||
|
}
|
||||||
|
|
||||||
private func open(tab: SettingsTab) {
|
private func open(tab: SettingsTab) {
|
||||||
SettingsTabRouter.request(tab)
|
SettingsTabRouter.request(tab)
|
||||||
NSApp.activate(ignoringOtherApps: true)
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
|
|
@ -561,28 +631,6 @@ struct MenuContent: View {
|
||||||
return "System default"
|
return "System default"
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
|
||||||
private func reloadSessionMenu() async {
|
|
||||||
do {
|
|
||||||
let snapshot = try await SessionLoader.loadSnapshot(limit: 32)
|
|
||||||
self.sessionStorePath = snapshot.storePath
|
|
||||||
let now = Date()
|
|
||||||
let active = snapshot.rows.filter { row in
|
|
||||||
if row.key == "main" { return true }
|
|
||||||
guard let updatedAt = row.updatedAt else { return false }
|
|
||||||
return now.timeIntervalSince(updatedAt) <= self.sessionMenuActiveWindowSeconds
|
|
||||||
}
|
|
||||||
self.sessionMenu = active.sorted { lhs, rhs in
|
|
||||||
if lhs.key == "main" { return true }
|
|
||||||
if rhs.key == "main" { return false }
|
|
||||||
return (lhs.updatedAt ?? .distantPast) > (rhs.updatedAt ?? .distantPast)
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
self.sessionStorePath = nil
|
|
||||||
self.sessionMenu = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private func loadMicrophones(force: Bool = false) async {
|
private func loadMicrophones(force: Bool = false) async {
|
||||||
guard self.showVoiceWakeMicPicker else {
|
guard self.showVoiceWakeMicPicker else {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct MenuSessionsHeaderView: View {
|
||||||
|
let count: Int
|
||||||
|
let statusText: String?
|
||||||
|
|
||||||
|
private let paddingTop: CGFloat = 8
|
||||||
|
private let paddingBottom: CGFloat = 6
|
||||||
|
private let paddingTrailing: CGFloat = 10
|
||||||
|
private let paddingLeading: CGFloat = 20
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
|
HStack(alignment: .firstTextBaseline) {
|
||||||
|
Text("Context")
|
||||||
|
.font(.caption.weight(.semibold))
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
Spacer(minLength: 10)
|
||||||
|
Text(self.subtitle)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let statusText, !statusText.isEmpty {
|
||||||
|
Text(statusText)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
.truncationMode(.tail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.top, self.paddingTop)
|
||||||
|
.padding(.bottom, self.paddingBottom)
|
||||||
|
.padding(.leading, self.paddingLeading)
|
||||||
|
.padding(.trailing, self.paddingTrailing)
|
||||||
|
.frame(minWidth: 300, maxWidth: .infinity, alignment: .leading)
|
||||||
|
.transaction { txn in txn.animation = nil }
|
||||||
|
}
|
||||||
|
|
||||||
|
private var subtitle: String {
|
||||||
|
if self.count == 1 { return "1 session · 24h" }
|
||||||
|
return "\(self.count) sessions · 24h"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,14 +3,15 @@ import SwiftUI
|
||||||
struct SessionMenuLabelView: View {
|
struct SessionMenuLabelView: View {
|
||||||
let row: SessionRow
|
let row: SessionRow
|
||||||
let width: CGFloat
|
let width: CGFloat
|
||||||
private let horizontalPadding: CGFloat = 8
|
private let paddingLeading: CGFloat = 20
|
||||||
|
private let paddingTrailing: CGFloat = 10
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 5) {
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
ContextUsageBar(
|
ContextUsageBar(
|
||||||
usedTokens: row.tokens.total,
|
usedTokens: row.tokens.total,
|
||||||
contextTokens: row.tokens.contextTokens,
|
contextTokens: row.tokens.contextTokens,
|
||||||
width: max(1, self.width - (self.horizontalPadding * 2)),
|
width: max(1, self.width - (self.paddingLeading + self.paddingTrailing)),
|
||||||
height: 4)
|
height: 4)
|
||||||
|
|
||||||
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
||||||
|
|
@ -31,6 +32,7 @@ struct SessionMenuLabelView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.vertical, 4)
|
.padding(.vertical, 4)
|
||||||
.padding(.horizontal, self.horizontalPadding)
|
.padding(.leading, self.paddingLeading)
|
||||||
|
.padding(.trailing, self.paddingTrailing)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue