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)
.font(AtlasTypography.captionSmall)
.foregroundStyle(.secondary)
.lineLimit(2)
.lineLimit(1)
.truncationMode(.tail)
}
} icon: {
ZStack {

View File

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

View File

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

View File

@@ -135,7 +135,7 @@ public enum AtlasSpacing {
/// 24pt screen-level vertical rhythm.
public static let xxl: CGFloat = 24
/// 28pt screen horizontal margin.
public static let screenH: CGFloat = 28
public static let screenH: CGFloat = 24
/// 32pt large section separation.
public static let section: CGFloat = 32
}
@@ -227,21 +227,21 @@ public enum AtlasMotion {
/// Shared layout constants.
public enum AtlasLayout {
/// 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.
public static let metricColumns: [GridItem] = [
GridItem(.flexible(minimum: 220), spacing: AtlasSpacing.lg),
GridItem(.flexible(minimum: 220), spacing: AtlasSpacing.lg),
GridItem(.flexible(minimum: 220), spacing: AtlasSpacing.lg),
GridItem(.flexible(minimum: 180), spacing: AtlasSpacing.lg),
GridItem(.flexible(minimum: 180), spacing: AtlasSpacing.lg),
GridItem(.flexible(minimum: 180), spacing: AtlasSpacing.lg),
]
/// 2-column grid for wider cards.
public static let wideColumns: [GridItem] = [
GridItem(.flexible(minimum: 300), spacing: AtlasSpacing.lg),
GridItem(.flexible(minimum: 300), spacing: AtlasSpacing.lg),
GridItem(.flexible(minimum: 220), spacing: AtlasSpacing.lg),
GridItem(.flexible(minimum: 220), spacing: AtlasSpacing.lg),
]
/// Sidebar width range.
public static let sidebarMinWidth: CGFloat = 230
public static let sidebarIdealWidth: CGFloat = 260
public static let sidebarMinWidth: CGFloat = 180
public static let sidebarIdealWidth: CGFloat = 220
/// Sidebar icon container size (pill-style like System Settings).
public static let sidebarIconSize: CGFloat = 32
}

View File

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

View File

@@ -105,8 +105,8 @@ public struct AppsFeatureView: View {
tone: selectedAppMatchingPreview == nil ? .neutral : .warning
) {
GeometryReader { proxy in
let isWide = proxy.size.width >= 760
let sidebarWidth = min(max(proxy.size.width * 0.32, 260), 300)
let isWide = proxy.size.width >= 680
let sidebarWidth = min(max(proxy.size.width * 0.3, 220), 280)
Group {
if isWide {
@@ -455,36 +455,50 @@ private struct AppDetailView: View {
}
}
HStack(alignment: .center, spacing: AtlasSpacing.md) {
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)
}
ViewThatFits(in: .horizontal) {
HStack(alignment: .center, spacing: AtlasSpacing.md) {
previewButton
uninstallButton
}
.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")) {
onUninstall()
VStack(alignment: .leading, spacing: AtlasSpacing.md) {
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)
}
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 {
switch kind {
case .removeCache:

View File

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

View File

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

View File

@@ -40,22 +40,24 @@ public struct SettingsFeatureView: View {
subtitle: AtlasL10n.string("settings.panel.subtitle")
) {
VStack(alignment: .leading, spacing: AtlasSpacing.xl) {
HStack(spacing: AtlasSpacing.sm) {
ForEach(SettingsPanel.allCases) { panel in
Group {
if selectedPanel == panel {
Button(panel.title) {
selectedPanel = panel
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: AtlasSpacing.sm) {
ForEach(SettingsPanel.allCases) { panel in
Group {
if 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)
.foregroundStyle(.secondary)
HStack(alignment: .center, spacing: AtlasSpacing.md) {
Button(AtlasL10n.string("settings.trust.documents.ack")) {
presentedDocument = .acknowledgement(settings.acknowledgementText)
ViewThatFits(in: .horizontal) {
HStack(alignment: .center, spacing: AtlasSpacing.md) {
trustDocumentButtons
}
.buttonStyle(.atlasSecondary)
Button(AtlasL10n.string("settings.trust.documents.notices")) {
presentedDocument = .notices(settings.thirdPartyNoticesText)
VStack(alignment: .leading, spacing: AtlasSpacing.md) {
trustDocumentButtons
}
.buttonStyle(.atlasSecondary)
}
}
}
.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 {

View File

@@ -86,38 +86,20 @@ public struct SmartCleanFeatureView: View {
systemImage: primaryAction.systemImage
)
HStack(alignment: .center, spacing: AtlasSpacing.md) {
Button(action: primaryAction.handler(startScan: onStartScan, refreshPreview: onRefreshPreview, executePlan: onExecutePlan)) {
Label(primaryAction.buttonTitle, systemImage: primaryAction.buttonSystemImage)
ViewThatFits(in: .horizontal) {
HStack(alignment: .center, spacing: AtlasSpacing.md) {
primaryActionButton
supportingActionButtons
Spacer(minLength: 0)
}
.buttonStyle(.atlasPrimary)
.keyboardShortcut(.defaultAction)
.disabled(primaryAction.isDisabled(canExecutePlan: canExecutePlan))
.accessibilityIdentifier(primaryAction.accessibilityIdentifier)
.accessibilityHint(primaryAction.accessibilityHint)
if primaryAction != .scan {
Button(action: onStartScan) {
Label(AtlasL10n.string("smartclean.action.runScan"), systemImage: "sparkles")
VStack(alignment: .leading, spacing: AtlasSpacing.md) {
primaryActionButton
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
}
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 {
switch kind {
case .removeCache: