Some checks are pending
CI / test (push) Waiting to run
Tighten the inbox, detail, and watch layouts so approval actions feel denser and more direct across compact surfaces.
170 lines
5.5 KiB
Swift
170 lines
5.5 KiB
Swift
import SwiftUI
|
|
|
|
/// Legacy theme shim kept only so AppComponents.swift still compiles while
|
|
/// the codebase has finished migrating to `IdP` / `Color.idp*` tokens.
|
|
/// Do not reference these from new code — use `IdP.tint`, `Color.idpGroupedBackground`,
|
|
/// etc. from `Sources/Core/Design/` instead.
|
|
enum AppTheme {
|
|
static let accent: Color = IdP.tint
|
|
static let warmAccent: Color = .orange
|
|
static let border: Color = Color.idpSeparator
|
|
static let shadow: Color = Color.black.opacity(0.08)
|
|
static let cardFill: Color = Color.idpSecondaryGroupedBackground
|
|
static let mutedFill: Color = Color.idpSecondaryGroupedBackground
|
|
static let backgroundTop: Color = Color.idpGroupedBackground
|
|
static let backgroundBottom: Color = Color.idpGroupedBackground
|
|
static let backgroundGlow: Color = .clear
|
|
static let chromeFill: Color = Color.idpSecondaryGroupedBackground
|
|
}
|
|
|
|
enum AppLayout {
|
|
static let compactHorizontalPadding: CGFloat = 16
|
|
static let regularHorizontalPadding: CGFloat = 28
|
|
static let compactVerticalPadding: CGFloat = 18
|
|
static let regularVerticalPadding: CGFloat = 28
|
|
static let compactContentWidth: CGFloat = 720
|
|
static let regularContentWidth: CGFloat = 920
|
|
static let cardRadius: CGFloat = IdP.cardRadius
|
|
static let largeCardRadius: CGFloat = 28
|
|
static let compactSectionPadding: CGFloat = 18
|
|
static let regularSectionPadding: CGFloat = 24
|
|
static let compactSectionSpacing: CGFloat = 18
|
|
static let regularSectionSpacing: CGFloat = 24
|
|
static let compactBottomDockPadding: CGFloat = 120
|
|
static let regularBottomPadding: CGFloat = 56
|
|
|
|
static func horizontalPadding(for compactLayout: Bool) -> CGFloat {
|
|
compactLayout ? compactHorizontalPadding : regularHorizontalPadding
|
|
}
|
|
|
|
static func verticalPadding(for compactLayout: Bool) -> CGFloat {
|
|
compactLayout ? compactVerticalPadding : regularVerticalPadding
|
|
}
|
|
|
|
static func contentWidth(for compactLayout: Bool) -> CGFloat {
|
|
compactLayout ? compactContentWidth : regularContentWidth
|
|
}
|
|
|
|
static func sectionPadding(for compactLayout: Bool) -> CGFloat {
|
|
compactLayout ? compactSectionPadding : regularSectionPadding
|
|
}
|
|
|
|
static func sectionSpacing(for compactLayout: Bool) -> CGFloat {
|
|
compactLayout ? compactSectionSpacing : regularSectionSpacing
|
|
}
|
|
}
|
|
|
|
extension View {
|
|
func appSurface(radius: CGFloat = AppLayout.cardRadius, fill: Color = AppTheme.cardFill) -> some View {
|
|
background(
|
|
fill,
|
|
in: RoundedRectangle(cornerRadius: radius, style: .continuous)
|
|
)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: radius, style: .continuous)
|
|
.stroke(AppTheme.border, lineWidth: 0.5)
|
|
)
|
|
}
|
|
}
|
|
|
|
struct AppBackground: View {
|
|
var body: some View {
|
|
Color.idpGroupedBackground.ignoresSafeArea()
|
|
}
|
|
}
|
|
|
|
struct AppScrollScreen<Content: View>: View {
|
|
let compactLayout: Bool
|
|
var bottomPadding: CGFloat? = nil
|
|
let content: () -> Content
|
|
|
|
init(
|
|
compactLayout: Bool,
|
|
bottomPadding: CGFloat? = nil,
|
|
@ViewBuilder content: @escaping () -> Content
|
|
) {
|
|
self.compactLayout = compactLayout
|
|
self.bottomPadding = bottomPadding
|
|
self.content = content
|
|
}
|
|
|
|
var body: some View {
|
|
ScrollView {
|
|
VStack(alignment: .leading, spacing: AppLayout.sectionSpacing(for: compactLayout)) {
|
|
content()
|
|
}
|
|
.frame(maxWidth: AppLayout.contentWidth(for: compactLayout), alignment: .leading)
|
|
.padding(.horizontal, AppLayout.horizontalPadding(for: compactLayout))
|
|
.padding(.top, AppLayout.verticalPadding(for: compactLayout))
|
|
.padding(.bottom, bottomPadding ?? AppLayout.verticalPadding(for: compactLayout))
|
|
.frame(maxWidth: .infinity, alignment: compactLayout ? .leading : .center)
|
|
}
|
|
.scrollIndicators(.hidden)
|
|
}
|
|
}
|
|
|
|
struct AppPanel<Content: View>: View {
|
|
let compactLayout: Bool
|
|
let radius: CGFloat
|
|
let content: () -> Content
|
|
|
|
init(
|
|
compactLayout: Bool,
|
|
radius: CGFloat = AppLayout.cardRadius,
|
|
@ViewBuilder content: @escaping () -> Content
|
|
) {
|
|
self.compactLayout = compactLayout
|
|
self.radius = radius
|
|
self.content = content
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 14) {
|
|
content()
|
|
}
|
|
.padding(AppLayout.sectionPadding(for: compactLayout))
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.appSurface(radius: radius)
|
|
}
|
|
}
|
|
|
|
struct AppBadge: View {
|
|
let title: String
|
|
var tone: Color = AppTheme.accent
|
|
|
|
var body: some View {
|
|
Text(title)
|
|
.font(.caption.weight(.semibold))
|
|
.foregroundStyle(tone)
|
|
.padding(.horizontal, 12)
|
|
.padding(.vertical, 6)
|
|
.background(tone.opacity(0.14), in: Capsule())
|
|
}
|
|
}
|
|
|
|
struct AppSectionCard<Content: View>: View {
|
|
let title: String
|
|
var subtitle: String? = nil
|
|
let compactLayout: Bool
|
|
let content: () -> Content
|
|
|
|
init(
|
|
title: String,
|
|
subtitle: String? = nil,
|
|
compactLayout: Bool,
|
|
@ViewBuilder content: @escaping () -> Content
|
|
) {
|
|
self.title = title
|
|
self.subtitle = subtitle
|
|
self.compactLayout = compactLayout
|
|
self.content = content
|
|
}
|
|
|
|
var body: some View {
|
|
AppPanel(compactLayout: compactLayout) {
|
|
AppSectionTitle(title: title, subtitle: subtitle)
|
|
content()
|
|
}
|
|
}
|
|
}
|