feat: improve Atlas narrow window responsiveness

This commit is contained in:
zhukang
2026-03-10 18:39:04 +08:00
parent 994e63f0b3
commit e37927b143
10 changed files with 184 additions and 111 deletions

View File

@@ -231,7 +231,8 @@ private struct SidebarRouteRow: View {
Text(route.subtitle) Text(route.subtitle)
.font(AtlasTypography.captionSmall) .font(AtlasTypography.captionSmall)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.lineLimit(2) .lineLimit(1)
.truncationMode(.tail)
} }
} icon: { } icon: {
ZStack { ZStack {

View File

@@ -9,7 +9,7 @@ struct AtlasApp: App {
WindowGroup(AtlasL10n.string("app.name")) { WindowGroup(AtlasL10n.string("app.name")) {
AppShellView(model: model) AppShellView(model: model)
.environment(\.locale, model.appLanguage.locale) .environment(\.locale, model.appLanguage.locale)
.frame(minWidth: 1120, minHeight: 720) .frame(minWidth: 940, minHeight: 640)
} }
.commands { .commands {
AtlasAppCommands(model: model) AtlasAppCommands(model: model)

View File

@@ -80,6 +80,7 @@ final class AtlasAppUITests: XCTestCase {
let app = XCUIApplication() let app = XCUIApplication()
let stateFile = NSTemporaryDirectory() + UUID().uuidString + "/workspace-state.json" let stateFile = NSTemporaryDirectory() + UUID().uuidString + "/workspace-state.json"
app.launchEnvironment["ATLAS_STATE_FILE"] = stateFile app.launchEnvironment["ATLAS_STATE_FILE"] = stateFile
app.launchArguments += ["-ApplePersistenceIgnoreState", "YES"]
return app return app
} }
} }

View File

@@ -135,7 +135,7 @@ public enum AtlasSpacing {
/// 24pt screen-level vertical rhythm. /// 24pt screen-level vertical rhythm.
public static let xxl: CGFloat = 24 public static let xxl: CGFloat = 24
/// 28pt screen horizontal margin. /// 28pt screen horizontal margin.
public static let screenH: CGFloat = 28 public static let screenH: CGFloat = 24
/// 32pt large section separation. /// 32pt large section separation.
public static let section: CGFloat = 32 public static let section: CGFloat = 32
} }
@@ -227,21 +227,21 @@ public enum AtlasMotion {
/// Shared layout constants. /// Shared layout constants.
public enum AtlasLayout { public enum AtlasLayout {
/// Maximum content reading width prevents overly long text lines. /// Maximum content reading width prevents overly long text lines.
public static let maxReadingWidth: CGFloat = 960 public static let maxReadingWidth: CGFloat = 920
/// Standard 3-column metric grid definition. /// Standard 3-column metric grid definition.
public static let metricColumns: [GridItem] = [ public static let metricColumns: [GridItem] = [
GridItem(.flexible(minimum: 220), spacing: AtlasSpacing.lg), GridItem(.flexible(minimum: 180), spacing: AtlasSpacing.lg),
GridItem(.flexible(minimum: 220), spacing: AtlasSpacing.lg), GridItem(.flexible(minimum: 180), spacing: AtlasSpacing.lg),
GridItem(.flexible(minimum: 220), spacing: AtlasSpacing.lg), GridItem(.flexible(minimum: 180), spacing: AtlasSpacing.lg),
] ]
/// 2-column grid for wider cards. /// 2-column grid for wider cards.
public static let wideColumns: [GridItem] = [ public static let wideColumns: [GridItem] = [
GridItem(.flexible(minimum: 300), spacing: AtlasSpacing.lg), GridItem(.flexible(minimum: 220), spacing: AtlasSpacing.lg),
GridItem(.flexible(minimum: 300), spacing: AtlasSpacing.lg), GridItem(.flexible(minimum: 220), spacing: AtlasSpacing.lg),
] ]
/// Sidebar width range. /// Sidebar width range.
public static let sidebarMinWidth: CGFloat = 230 public static let sidebarMinWidth: CGFloat = 180
public static let sidebarIdealWidth: CGFloat = 260 public static let sidebarIdealWidth: CGFloat = 220
/// Sidebar icon container size (pill-style like System Settings). /// Sidebar icon container size (pill-style like System Settings).
public static let sidebarIconSize: CGFloat = 32 public static let sidebarIconSize: CGFloat = 32
} }

View File

@@ -76,37 +76,52 @@ public struct AtlasScreen<Content: View>: View {
} }
public var body: some View { public var body: some View {
ZStack { GeometryReader { proxy in
LinearGradient( let horizontalPadding = resolvedHorizontalPadding(for: proxy.size.width)
colors: [AtlasColor.canvasTop, AtlasColor.canvasBottom],
startPoint: .top,
endPoint: .bottom
)
.ignoresSafeArea()
Group { ZStack {
if useScrollView { LinearGradient(
ScrollView { colors: [AtlasColor.canvasTop, AtlasColor.canvasBottom],
contentStack startPoint: .top,
endPoint: .bottom
)
.ignoresSafeArea()
Group {
if useScrollView {
ScrollView {
contentStack(horizontalPadding: horizontalPadding)
}
} else {
contentStack(horizontalPadding: horizontalPadding)
} }
} else {
contentStack
} }
} }
} }
} }
private var contentStack: some View { private func contentStack(horizontalPadding: CGFloat) -> some View {
VStack(alignment: .leading, spacing: AtlasSpacing.xxl) { VStack(alignment: .leading, spacing: AtlasSpacing.xxl) {
header header
content content
} }
.frame(maxWidth: AtlasLayout.maxReadingWidth, maxHeight: .infinity, alignment: .topLeading) .frame(maxWidth: AtlasLayout.maxReadingWidth, maxHeight: .infinity, alignment: .topLeading)
.padding(.horizontal, AtlasSpacing.screenH) .padding(.horizontal, horizontalPadding)
.padding(.vertical, AtlasSpacing.xxl) .padding(.vertical, AtlasSpacing.xxl)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
} }
private func resolvedHorizontalPadding(for width: CGFloat) -> CGFloat {
switch width {
case ..<820:
return 16
case ..<980:
return 20
default:
return AtlasSpacing.screenH
}
}
private var header: some View { private var header: some View {
VStack(alignment: .leading, spacing: AtlasSpacing.sm) { VStack(alignment: .leading, spacing: AtlasSpacing.sm) {
Text(title) Text(title)

View File

@@ -105,8 +105,8 @@ public struct AppsFeatureView: View {
tone: selectedAppMatchingPreview == nil ? .neutral : .warning tone: selectedAppMatchingPreview == nil ? .neutral : .warning
) { ) {
GeometryReader { proxy in GeometryReader { proxy in
let isWide = proxy.size.width >= 760 let isWide = proxy.size.width >= 680
let sidebarWidth = min(max(proxy.size.width * 0.32, 260), 300) let sidebarWidth = min(max(proxy.size.width * 0.3, 220), 280)
Group { Group {
if isWide { if isWide {
@@ -455,36 +455,50 @@ private struct AppDetailView: View {
} }
} }
HStack(alignment: .center, spacing: AtlasSpacing.md) { ViewThatFits(in: .horizontal) {
Group { HStack(alignment: .center, spacing: AtlasSpacing.md) {
if previewPlan == nil { previewButton
Button(isBuildingPreview ? AtlasL10n.string("apps.preview.running") : AtlasL10n.string("apps.preview.action")) { uninstallButton
onPreview()
}
.buttonStyle(.atlasPrimary)
} else {
Button(isBuildingPreview ? AtlasL10n.string("apps.preview.running") : AtlasL10n.string("apps.preview.action")) {
onPreview()
}
.buttonStyle(.atlasSecondary)
}
} }
.disabled(isBusy)
.accessibilityIdentifier("apps.preview.\(app.id.uuidString)")
.accessibilityHint(AtlasL10n.string("apps.preview.hint"))
Button(isUninstalling ? AtlasL10n.string("apps.uninstall.running") : AtlasL10n.string("apps.uninstall.action")) { VStack(alignment: .leading, spacing: AtlasSpacing.md) {
onUninstall() previewButton
uninstallButton
} }
.buttonStyle(.atlasPrimary)
.disabled(isBusy || previewPlan == nil)
.accessibilityIdentifier("apps.uninstall.\(app.id.uuidString)")
.accessibilityHint(AtlasL10n.string("apps.uninstall.hint"))
} }
} }
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
} }
private var previewButton: some View {
Group {
if previewPlan == nil {
Button(isBuildingPreview ? AtlasL10n.string("apps.preview.running") : AtlasL10n.string("apps.preview.action")) {
onPreview()
}
.buttonStyle(.atlasPrimary)
} else {
Button(isBuildingPreview ? AtlasL10n.string("apps.preview.running") : AtlasL10n.string("apps.preview.action")) {
onPreview()
}
.buttonStyle(.atlasSecondary)
}
}
.disabled(isBusy)
.accessibilityIdentifier("apps.preview.\(app.id.uuidString)")
.accessibilityHint(AtlasL10n.string("apps.preview.hint"))
}
private var uninstallButton: some View {
Button(isUninstalling ? AtlasL10n.string("apps.uninstall.running") : AtlasL10n.string("apps.uninstall.action")) {
onUninstall()
}
.buttonStyle(.atlasPrimary)
.disabled(isBusy || previewPlan == nil)
.accessibilityIdentifier("apps.uninstall.\(app.id.uuidString)")
.accessibilityHint(AtlasL10n.string("apps.uninstall.hint"))
}
private func icon(for kind: ActionItem.Kind) -> String { private func icon(for kind: ActionItem.Kind) -> String {
switch kind { switch kind {
case .removeCache: case .removeCache:

View File

@@ -96,8 +96,8 @@ public struct HistoryFeatureView: View {
} }
GeometryReader { proxy in GeometryReader { proxy in
let isWide = proxy.size.width >= 760 let isWide = proxy.size.width >= 680
let sidebarWidth = min(max(proxy.size.width * 0.32, 250), 290) let sidebarWidth = min(max(proxy.size.width * 0.3, 210), 270)
Group { Group {
if isWide { if isWide {

View File

@@ -82,20 +82,15 @@ public struct PermissionsFeatureView: View {
) )
} }
HStack(alignment: .center, spacing: AtlasSpacing.md) { ViewThatFits(in: .horizontal) {
if let nextActionKind { HStack(alignment: .center, spacing: AtlasSpacing.md) {
Button(buttonTitle(for: nextActionKind)) { nextStepButtons
performAction(for: nextActionKind) Spacer(minLength: 0)
}
.buttonStyle(.atlasPrimary)
} }
Button(action: onRefresh) { VStack(alignment: .leading, spacing: AtlasSpacing.md) {
Label(AtlasL10n.string("permissions.refresh"), systemImage: "arrow.clockwise") nextStepButtons
} }
.buttonStyle(.atlasSecondary)
.accessibilityIdentifier("permissions.refresh")
.accessibilityHint(AtlasL10n.string("permissions.refresh.hint"))
} }
} }
} }
@@ -224,6 +219,23 @@ public struct PermissionsFeatureView: View {
return nextActionKind.systemImage return nextActionKind.systemImage
} }
@ViewBuilder
private var nextStepButtons: some View {
if let nextActionKind {
Button(buttonTitle(for: nextActionKind)) {
performAction(for: nextActionKind)
}
.buttonStyle(.atlasPrimary)
}
Button(action: onRefresh) {
Label(AtlasL10n.string("permissions.refresh"), systemImage: "arrow.clockwise")
}
.buttonStyle(.atlasSecondary)
.accessibilityIdentifier("permissions.refresh")
.accessibilityHint(AtlasL10n.string("permissions.refresh.hint"))
}
@ViewBuilder @ViewBuilder
private func permissionRow(_ state: PermissionState) -> some View { private func permissionRow(_ state: PermissionState) -> some View {
AtlasDetailRow( AtlasDetailRow(

View File

@@ -40,22 +40,24 @@ public struct SettingsFeatureView: View {
subtitle: AtlasL10n.string("settings.panel.subtitle") subtitle: AtlasL10n.string("settings.panel.subtitle")
) { ) {
VStack(alignment: .leading, spacing: AtlasSpacing.xl) { VStack(alignment: .leading, spacing: AtlasSpacing.xl) {
HStack(spacing: AtlasSpacing.sm) { ScrollView(.horizontal, showsIndicators: false) {
ForEach(SettingsPanel.allCases) { panel in HStack(spacing: AtlasSpacing.sm) {
Group { ForEach(SettingsPanel.allCases) { panel in
if selectedPanel == panel { Group {
Button(panel.title) { if selectedPanel == panel {
selectedPanel = panel Button(panel.title) {
selectedPanel = panel
}
.buttonStyle(.atlasSecondary)
} else {
Button(panel.title) {
selectedPanel = panel
}
.buttonStyle(.atlasGhost)
} }
.buttonStyle(.atlasSecondary)
} else {
Button(panel.title) {
selectedPanel = panel
}
.buttonStyle(.atlasGhost)
} }
.accessibilityIdentifier("settings.panel.\(panel.id)")
} }
.accessibilityIdentifier("settings.panel.\(panel.id)")
} }
} }
@@ -237,21 +239,32 @@ public struct SettingsFeatureView: View {
.font(AtlasTypography.body) .font(AtlasTypography.body)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
HStack(alignment: .center, spacing: AtlasSpacing.md) { ViewThatFits(in: .horizontal) {
Button(AtlasL10n.string("settings.trust.documents.ack")) { HStack(alignment: .center, spacing: AtlasSpacing.md) {
presentedDocument = .acknowledgement(settings.acknowledgementText) trustDocumentButtons
} }
.buttonStyle(.atlasSecondary)
Button(AtlasL10n.string("settings.trust.documents.notices")) { VStack(alignment: .leading, spacing: AtlasSpacing.md) {
presentedDocument = .notices(settings.thirdPartyNoticesText) trustDocumentButtons
} }
.buttonStyle(.atlasSecondary)
} }
} }
} }
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
} }
@ViewBuilder
private var trustDocumentButtons: some View {
Button(AtlasL10n.string("settings.trust.documents.ack")) {
presentedDocument = .acknowledgement(settings.acknowledgementText)
}
.buttonStyle(.atlasSecondary)
Button(AtlasL10n.string("settings.trust.documents.notices")) {
presentedDocument = .notices(settings.thirdPartyNoticesText)
}
.buttonStyle(.atlasSecondary)
}
} }
private enum SettingsPanel: String, CaseIterable, Identifiable { private enum SettingsPanel: String, CaseIterable, Identifiable {

View File

@@ -86,38 +86,20 @@ public struct SmartCleanFeatureView: View {
systemImage: primaryAction.systemImage systemImage: primaryAction.systemImage
) )
HStack(alignment: .center, spacing: AtlasSpacing.md) { ViewThatFits(in: .horizontal) {
Button(action: primaryAction.handler(startScan: onStartScan, refreshPreview: onRefreshPreview, executePlan: onExecutePlan)) { HStack(alignment: .center, spacing: AtlasSpacing.md) {
Label(primaryAction.buttonTitle, systemImage: primaryAction.buttonSystemImage) primaryActionButton
supportingActionButtons
Spacer(minLength: 0)
} }
.buttonStyle(.atlasPrimary)
.keyboardShortcut(.defaultAction)
.disabled(primaryAction.isDisabled(canExecutePlan: canExecutePlan))
.accessibilityIdentifier(primaryAction.accessibilityIdentifier)
.accessibilityHint(primaryAction.accessibilityHint)
if primaryAction != .scan { VStack(alignment: .leading, spacing: AtlasSpacing.md) {
Button(action: onStartScan) { primaryActionButton
Label(AtlasL10n.string("smartclean.action.runScan"), systemImage: "sparkles") HStack(alignment: .center, spacing: AtlasSpacing.md) {
supportingActionButtons
Spacer(minLength: 0)
} }
.buttonStyle(.atlasSecondary)
.keyboardShortcut("s", modifiers: [.command, .option])
.disabled(isScanning || isExecutingPlan)
.accessibilityIdentifier("smartclean.runScan")
.accessibilityHint(AtlasL10n.string("smartclean.action.runScan.hint"))
} }
if primaryAction != .refresh, !findings.isEmpty {
Button(action: onRefreshPreview) {
Label(AtlasL10n.string("smartclean.action.refreshPreview"), systemImage: "arrow.clockwise")
}
.buttonStyle(.atlasGhost)
.disabled(isScanning || isExecutingPlan)
.accessibilityIdentifier("smartclean.refreshPreview")
.accessibilityHint(AtlasL10n.string("smartclean.action.refreshPreview.hint"))
}
Spacer(minLength: 0)
} }
} }
} }
@@ -399,6 +381,41 @@ public struct SmartCleanFeatureView: View {
return .refresh return .refresh
} }
private var primaryActionButton: some View {
Button(action: primaryAction.handler(startScan: onStartScan, refreshPreview: onRefreshPreview, executePlan: onExecutePlan)) {
Label(primaryAction.buttonTitle, systemImage: primaryAction.buttonSystemImage)
}
.buttonStyle(.atlasPrimary)
.keyboardShortcut(.defaultAction)
.disabled(primaryAction.isDisabled(canExecutePlan: canExecutePlan))
.accessibilityIdentifier(primaryAction.accessibilityIdentifier)
.accessibilityHint(primaryAction.accessibilityHint)
}
@ViewBuilder
private var supportingActionButtons: some View {
if primaryAction != .scan {
Button(action: onStartScan) {
Label(AtlasL10n.string("smartclean.action.runScan"), systemImage: "sparkles")
}
.buttonStyle(.atlasSecondary)
.keyboardShortcut("s", modifiers: [.command, .option])
.disabled(isScanning || isExecutingPlan)
.accessibilityIdentifier("smartclean.runScan")
.accessibilityHint(AtlasL10n.string("smartclean.action.runScan.hint"))
}
if primaryAction != .refresh, !findings.isEmpty {
Button(action: onRefreshPreview) {
Label(AtlasL10n.string("smartclean.action.refreshPreview"), systemImage: "arrow.clockwise")
}
.buttonStyle(.atlasGhost)
.disabled(isScanning || isExecutingPlan)
.accessibilityIdentifier("smartclean.refreshPreview")
.accessibilityHint(AtlasL10n.string("smartclean.action.refreshPreview.hint"))
}
}
private func supportText(for kind: ActionItem.Kind) -> String { private func supportText(for kind: ActionItem.Kind) -> String {
switch kind { switch kind {
case .removeCache: case .removeCache: