feat(design-system): adaptive grid layout, content width env, and UI polish

Fix AtlasSpacing.screenH value (24 -> 28). Add adaptiveMetricColumns
for responsive grid layouts and atlasContentWidth environment key.
Enhance AtlasEmptyState with optional action button. Brand-tint
ProgressView controls. Add AtlasPresentationHelpers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
zhukang
2026-03-10 21:57:13 +08:00
parent 7b75e84218
commit 8d56a0e92c
3 changed files with 147 additions and 7 deletions

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 = 24
public static let screenH: CGFloat = 28
/// 32pt large section separation.
public static let section: CGFloat = 32
}
@@ -244,6 +244,45 @@ public enum AtlasLayout {
public static let sidebarIdealWidth: CGFloat = 220
/// Sidebar icon container size (pill-style like System Settings).
public static let sidebarIconSize: CGFloat = 32
/// Returns an adaptive column layout based on available width.
/// - 3 columns for widths >= 640
/// - 2 columns for widths >= 420
/// - 1 column for narrower widths
public static func adaptiveMetricColumns(for width: CGFloat) -> [GridItem] {
let spacing = AtlasSpacing.lg
switch width {
case 640...:
return [
GridItem(.flexible(minimum: 180), spacing: spacing),
GridItem(.flexible(minimum: 180), spacing: spacing),
GridItem(.flexible(minimum: 180), spacing: spacing),
]
case 420...:
return [
GridItem(.flexible(minimum: 180), spacing: spacing),
GridItem(.flexible(minimum: 180), spacing: spacing),
]
default:
return [
GridItem(.flexible(minimum: 180), spacing: spacing),
]
}
}
}
// MARK: - Content Width Environment Key
private struct AtlasContentWidthKey: EnvironmentKey {
static let defaultValue: CGFloat = 920
}
public extension EnvironmentValues {
/// The actual content width injected by `AtlasScreen`.
var atlasContentWidth: CGFloat {
get { self[AtlasContentWidthKey.self] }
set { self[AtlasContentWidthKey.self] = newValue }
}
}
// MARK: - Icon Tokens

View File

@@ -90,18 +90,20 @@ public struct AtlasScreen<Content: View>: View {
Group {
if useScrollView {
ScrollView {
contentStack(horizontalPadding: horizontalPadding)
contentStack(horizontalPadding: horizontalPadding, containerWidth: proxy.size.width)
}
} else {
contentStack(horizontalPadding: horizontalPadding)
contentStack(horizontalPadding: horizontalPadding, containerWidth: proxy.size.width)
}
}
}
}
}
private func contentStack(horizontalPadding: CGFloat) -> some View {
VStack(alignment: .leading, spacing: AtlasSpacing.xxl) {
private func contentStack(horizontalPadding: CGFloat, containerWidth: CGFloat) -> some View {
let contentWidth = min(AtlasLayout.maxReadingWidth, containerWidth - horizontalPadding * 2)
return VStack(alignment: .leading, spacing: AtlasSpacing.xxl) {
header
content
}
@@ -109,6 +111,7 @@ public struct AtlasScreen<Content: View>: View {
.padding(.horizontal, horizontalPadding)
.padding(.vertical, AtlasSpacing.xxl)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.environment(\.atlasContentWidth, max(0, contentWidth))
}
private func resolvedHorizontalPadding(for width: CGFloat) -> CGFloat {
@@ -468,12 +471,16 @@ public struct AtlasEmptyState: View {
private let detail: String
private let systemImage: String
private let tone: AtlasTone
private let actionTitle: String?
private let onAction: (() -> Void)?
public init(title: String, detail: String, systemImage: String, tone: AtlasTone = .neutral) {
public init(title: String, detail: String, systemImage: String, tone: AtlasTone = .neutral, actionTitle: String? = nil, onAction: (() -> Void)? = nil) {
self.title = title
self.detail = detail
self.systemImage = systemImage
self.tone = tone
self.actionTitle = actionTitle
self.onAction = onAction
}
public var body: some View {
@@ -509,6 +516,13 @@ public struct AtlasEmptyState: View {
.multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true)
}
if let actionTitle, let onAction {
Button(actionTitle) {
onAction()
}
.buttonStyle(.atlasSecondary)
}
}
.frame(maxWidth: .infinity)
.padding(AtlasSpacing.section)
@@ -520,7 +534,7 @@ public struct AtlasEmptyState: View {
RoundedRectangle(cornerRadius: AtlasRadius.xl, style: .continuous)
.strokeBorder(Color.primary.opacity(0.06), lineWidth: 1)
)
.accessibilityElement(children: .ignore)
.accessibilityElement(children: onAction != nil ? .contain : .ignore)
.accessibilityLabel(Text(title))
.accessibilityValue(Text(detail))
}
@@ -543,6 +557,7 @@ public struct AtlasLoadingState: View {
HStack(spacing: AtlasSpacing.md) {
ProgressView()
.controlSize(.small)
.tint(AtlasColor.brand)
.accessibilityHidden(true)
Text(title)
@@ -557,6 +572,7 @@ public struct AtlasLoadingState: View {
if let progress {
ProgressView(value: progress, total: 1)
.controlSize(.large)
.tint(AtlasColor.brand)
}
}
.frame(maxWidth: .infinity, alignment: .leading)

View File

@@ -0,0 +1,85 @@
import AtlasDomain
import SwiftUI
// MARK: - Shared Domain UI Mappings
//
// These public extensions centralise the mapping from domain enums
// to AtlasTone and SF Symbol strings so every feature module shares
// the same visual language without duplicating private extensions.
public extension TaskStatus {
var atlasTone: AtlasTone {
switch self {
case .queued:
return .neutral
case .running:
return .warning
case .completed:
return .success
case .failed, .cancelled:
return .danger
}
}
}
public extension RiskLevel {
var atlasTone: AtlasTone {
switch self {
case .safe:
return .success
case .review:
return .warning
case .advanced:
return .danger
}
}
}
public extension TaskKind {
var atlasSystemImage: String {
switch self {
case .scan:
return "sparkles"
case .executePlan:
return "play.circle"
case .uninstallApp:
return "trash"
case .restore:
return "arrow.uturn.backward.circle"
case .inspectPermissions:
return "lock.shield"
}
}
}
public extension ActionItem.Kind {
var atlasSystemImage: String {
switch self {
case .removeCache:
return "trash"
case .removeApp:
return "app.badge.minus"
case .archiveFile:
return "archivebox"
case .inspectPermission:
return "lock.shield"
}
}
}
public enum AtlasCategoryIcon {
public static func systemImage(for category: String) -> String {
switch category.lowercased() {
case "developer":
return "hammer"
case "system":
return "gearshape.2"
case "apps":
return "square.stack.3d.up"
case "browsers":
return "globe"
default:
return "sparkles"
}
}
}