feat(app): add flow diagnostics and refine interface
Improve failure recovery visibility, strengthen account-session handling, and make the cross-platform interface more consistent.
This commit is contained in:
@@ -81,34 +81,134 @@ public struct SurfaceCardModifier: ViewModifier {
|
||||
@Environment(\.themePalette) private var palette
|
||||
|
||||
private let enabled: Bool
|
||||
private let elevated: Bool
|
||||
|
||||
public init(enabled: Bool = true) {
|
||||
public init(enabled: Bool = true, elevated: Bool = true) {
|
||||
self.enabled = enabled
|
||||
self.elevated = elevated
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
public func body(content: Content) -> some View {
|
||||
if enabled {
|
||||
let shape = RoundedRectangle(cornerRadius: Radius.xl, style: .continuous)
|
||||
content
|
||||
let card = content
|
||||
.background(
|
||||
palette.surface,
|
||||
elevated ? palette.surface : palette.formSurface,
|
||||
in: shape
|
||||
)
|
||||
// Clip child backgrounds as well as the card surface. Without
|
||||
// this, a full-width child can visually square off a corner.
|
||||
.clipShape(shape)
|
||||
if elevated {
|
||||
card.cardElevation()
|
||||
} else {
|
||||
card
|
||||
}
|
||||
} else {
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct CardElevationModifier: ViewModifier {
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
let accented: Bool
|
||||
|
||||
@ViewBuilder
|
||||
func body(content: Content) -> some View {
|
||||
#if os(iOS)
|
||||
// Layered low-opacity shadows create direction and length without
|
||||
// turning the warm background gray.
|
||||
content
|
||||
.shadow(
|
||||
color: nearShadowColor,
|
||||
radius: 1,
|
||||
x: 2,
|
||||
y: 3
|
||||
)
|
||||
.shadow(
|
||||
color: midShadowColor,
|
||||
radius: 3,
|
||||
x: 6,
|
||||
y: 8
|
||||
)
|
||||
.shadow(
|
||||
color: farShadowColor,
|
||||
radius: 9,
|
||||
x: 12,
|
||||
y: 18
|
||||
)
|
||||
#else
|
||||
content
|
||||
#endif
|
||||
}
|
||||
|
||||
private var nearShadowColor: Color {
|
||||
guard accented else { return OSGColor.cardShadowNear }
|
||||
return colorScheme == .dark
|
||||
? OSGColor.selectedCardShadowNearDark
|
||||
: OSGColor.selectedCardShadowNearLight
|
||||
}
|
||||
|
||||
private var midShadowColor: Color {
|
||||
guard accented else { return OSGColor.cardShadowMid }
|
||||
return colorScheme == .dark
|
||||
? OSGColor.selectedCardShadowMidDark
|
||||
: OSGColor.selectedCardShadowMidLight
|
||||
}
|
||||
|
||||
private var farShadowColor: Color {
|
||||
guard accented else { return OSGColor.cardShadowFar }
|
||||
return colorScheme == .dark
|
||||
? OSGColor.selectedCardShadowFarDark
|
||||
: OSGColor.selectedCardShadowFarLight
|
||||
}
|
||||
}
|
||||
|
||||
private struct CardListRowModifier: ViewModifier {
|
||||
let bottomSpacing: CGFloat
|
||||
let elevated: Bool
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.surfaceCard(elevated: elevated)
|
||||
.listRowInsets(
|
||||
EdgeInsets(
|
||||
top: 0,
|
||||
leading: 0,
|
||||
bottom: bottomSpacing,
|
||||
trailing: 0
|
||||
)
|
||||
)
|
||||
.listRowBackground(Color.clear)
|
||||
.listRowSeparator(.hidden)
|
||||
}
|
||||
}
|
||||
|
||||
public extension View {
|
||||
func cardSectionLabel() -> some View {
|
||||
modifier(CardSectionLabelModifier())
|
||||
}
|
||||
|
||||
func surfaceCard(enabled: Bool = true) -> some View {
|
||||
modifier(SurfaceCardModifier(enabled: enabled))
|
||||
func surfaceCard(
|
||||
enabled: Bool = true,
|
||||
elevated: Bool = true
|
||||
) -> some View {
|
||||
modifier(SurfaceCardModifier(enabled: enabled, elevated: elevated))
|
||||
}
|
||||
|
||||
func cardElevation(accented: Bool = false) -> some View {
|
||||
modifier(CardElevationModifier(accented: accented))
|
||||
}
|
||||
|
||||
/// Makes a native List/Form row render as an elevated app card while
|
||||
/// retaining system scrolling, swipe actions, refresh, and keyboard logic.
|
||||
func cardListRow(
|
||||
bottomSpacing: CGFloat = 0,
|
||||
elevated: Bool = true
|
||||
) -> some View {
|
||||
modifier(CardListRowModifier(bottomSpacing: bottomSpacing, elevated: elevated))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
// ColorLibrary.swift
|
||||
// OSGKeyboard · Shared Design System
|
||||
//
|
||||
// Canonical custom colour values used across the app, keyboard extension,
|
||||
// previews, and platform chrome. Views should prefer semantic ThemePalette
|
||||
// roles; use these fixed tokens only when a surface intentionally does not
|
||||
// follow the active app appearance.
|
||||
|
||||
import SwiftUI
|
||||
|
||||
public enum OSGColor {
|
||||
// MARK: - Brand
|
||||
|
||||
public static let brandAccent = color(0x3AA05A)
|
||||
public static let aiTeal = color(0x2BAFA4)
|
||||
/// Content that must remain white on colored or generated artwork.
|
||||
public static let fixedLightContent = Color.white
|
||||
public static let fixedDarkContent = Color.black
|
||||
|
||||
// MARK: - iOS app theme
|
||||
|
||||
public static let darkBackground = color(0x0A0A0B)
|
||||
public static let darkSurface = color(0x18181B)
|
||||
/// Interactive form fill: lighter than cards without looking elevated.
|
||||
public static let darkFormSurface = color(0x202024)
|
||||
public static let darkSurfaceElevated = color(0x27272A)
|
||||
public static let darkSurfaceMuted = color(0x121215)
|
||||
public static let darkDanger = color(0xFF453A)
|
||||
public static let darkWarning = color(0xFFBF18)
|
||||
public static let darkTextPrimary = color(0xFFFFFF)
|
||||
public static let darkTextSecondary = color(0xB3B3B3)
|
||||
public static let darkTextTertiary = color(0x808080)
|
||||
public static let darkDivider = Color.white.opacity(0.06)
|
||||
public static let darkDividerStrong = Color.white.opacity(0.10)
|
||||
|
||||
public static let lightBackground = color(0xF2F1EE)
|
||||
public static let lightSurface = color(0xFCFBF9)
|
||||
/// Light mode already has enough contrast, so forms retain the card fill.
|
||||
public static let lightFormSurface = lightSurface
|
||||
public static let lightSurfaceElevated = color(0xEBEAE7)
|
||||
public static let lightSurfaceMuted = color(0xEEEDE9)
|
||||
public static let lightDanger = color(0xFF3B30)
|
||||
public static let lightWarning = color(0xFF9E18)
|
||||
public static let lightTextPrimary = color(0x111118)
|
||||
public static let lightTextSecondary = color(0x64646F)
|
||||
public static let lightTextTertiary = color(0x8E8E9A)
|
||||
public static let lightDivider = Color.black.opacity(0.06)
|
||||
public static let lightDividerStrong = Color.black.opacity(0.10)
|
||||
|
||||
// MARK: - macOS app theme
|
||||
|
||||
public static let macDarkBackground = color(0x1C1C1E)
|
||||
public static let macDarkSurface = color(0x2C2C2E)
|
||||
public static let macDarkFormSurface = color(0x343438)
|
||||
public static let macDarkSurfaceElevated = color(0x3A3A3C)
|
||||
public static let macDarkSurfaceMuted = color(0x242426)
|
||||
public static let macDarkDivider = Color.white.opacity(0.08)
|
||||
public static let macDarkDividerStrong = Color.white.opacity(0.12)
|
||||
public static let macToggleKnob = Color.white
|
||||
public static let macToggleKnobShadow = Color.black.opacity(0.25)
|
||||
public static let macToggleOffTrackDark = Color.white.opacity(0.18)
|
||||
public static let macToggleOffTrackLight = Color.black.opacity(0.15)
|
||||
public static let macOverlayShadow = Color.black.opacity(0.22)
|
||||
|
||||
// MARK: - Recording and generated media
|
||||
|
||||
public static let recordRed = color(0xFF3B30)
|
||||
public static let recordBlue = color(0x007AFF)
|
||||
public static let recordingWaveform = color(0xFFC7C7)
|
||||
public static let recordDiscHairlineIdle = Color.white.opacity(0.08)
|
||||
public static let recordDiscHairlineActive = Color.white.opacity(0.12)
|
||||
public static let recordDiscOverlayStroke = Color.white.opacity(0.16)
|
||||
public static let recordListeningRing = Color.white.opacity(0.28)
|
||||
public static let pictureInPictureOutline = color(0xE5E7EB)
|
||||
public static let pictureInPictureAccent = color(0x33C78C)
|
||||
|
||||
// MARK: - Keyboard surfaces
|
||||
|
||||
public static let keyboardDarkKeyFill = color(0x525252)
|
||||
public static let keyboardDarkKeyPressed = color(0x3B3B3B)
|
||||
public static let keyboardDarkInputFill = color(0x4D4D4D)
|
||||
public static let keyboardDarkInputFillRaised = color(0x5C5C5C)
|
||||
public static let keyboardDarkControlFill = color(0x383838)
|
||||
public static let keyboardDarkControlBorder = color(0x1F1F1F)
|
||||
public static let keyboardLightKeyPressed = color(0xD6D6D6)
|
||||
public static let keyboardLightControlBorder = Color.black.opacity(0.12)
|
||||
public static let keyboardActionBorderDark = Color.black.opacity(0.10)
|
||||
public static let keyboardActionBorderLight = Color.black.opacity(0.08)
|
||||
public static let keyboardLightText = color(0x0F0F14)
|
||||
public static let keyboardDarkSend = color(0x49B96A)
|
||||
public static let keyboardLightSend = color(0x328C4C)
|
||||
public static let keyboardDarkSendPressed = brandAccent
|
||||
public static let keyboardLightSendPressed = color(0x28723F)
|
||||
|
||||
// MARK: - Fixed dark presentation surfaces
|
||||
|
||||
public static let demoBackground = color(0x0F0F12)
|
||||
public static let demoSelectionInactive = Color.gray
|
||||
public static let accountAvatarOutline = Color.white.opacity(0.20)
|
||||
public static let accountAvatarGradients: [[Color]] = [
|
||||
[.indigo, .blue],
|
||||
[.purple, .pink],
|
||||
[.teal, .cyan],
|
||||
[.orange, .red],
|
||||
[.mint, .green]
|
||||
]
|
||||
|
||||
// MARK: - Shared effects
|
||||
|
||||
public static let cardShadowNear = Color.black.opacity(0.018)
|
||||
public static let cardShadowMid = Color.black.opacity(0.018)
|
||||
public static let cardShadowFar = Color.black.opacity(0.025)
|
||||
public static let selectedCardFillLeadingDark = brandAccent.opacity(0.24)
|
||||
public static let selectedCardFillTrailingDark = brandAccent.opacity(0.15)
|
||||
public static let selectedCardFillLeadingLight = brandAccent.opacity(0.18)
|
||||
public static let selectedCardFillTrailingLight = brandAccent.opacity(0.11)
|
||||
public static let selectedCardStrokeDark = brandAccent.opacity(0.26)
|
||||
public static let selectedCardStrokeLight = brandAccent.opacity(0.18)
|
||||
public static let selectedCardShadowNearDark = brandAccent.opacity(0.12)
|
||||
public static let selectedCardShadowMidDark = brandAccent.opacity(0.075)
|
||||
public static let selectedCardShadowFarDark = brandAccent.opacity(0.035)
|
||||
public static let selectedCardShadowNearLight = brandAccent.opacity(0.075)
|
||||
public static let selectedCardShadowMidLight = brandAccent.opacity(0.05)
|
||||
public static let selectedCardShadowFarLight = brandAccent.opacity(0.03)
|
||||
|
||||
// MARK: - Animated mesh palettes
|
||||
|
||||
public static let meshAurora: [Color] = [
|
||||
color(0x0F1221), // top-left
|
||||
color(0x1A1433), // top-mid
|
||||
color(0x0D1A2E), // top-right
|
||||
color(0x1F1729), // left-mid
|
||||
color(0x2E1C38), // center — deepest violet
|
||||
color(0x141F33), // right-mid
|
||||
color(0x291A1A), // bottom-left — amber hint
|
||||
color(0x1A142E), // bottom-mid
|
||||
color(0x0D1724) // bottom-right
|
||||
]
|
||||
|
||||
public static let meshPolar: [Color] = [
|
||||
color(0x0D121A),
|
||||
color(0x171F2E),
|
||||
color(0x121724),
|
||||
color(0x1A212E),
|
||||
color(0x212938),
|
||||
color(0x141A29),
|
||||
color(0x0F1A29),
|
||||
color(0x171C2E),
|
||||
color(0x0D121F)
|
||||
]
|
||||
|
||||
public static let meshEmber: [Color] = [
|
||||
color(0x140F0D),
|
||||
color(0x24170F),
|
||||
color(0x1A120F),
|
||||
color(0x291A12),
|
||||
color(0x382114),
|
||||
color(0x1F1412),
|
||||
color(0x1A0F0D),
|
||||
color(0x26170F),
|
||||
color(0x140F0D)
|
||||
]
|
||||
|
||||
private static func color(_ hex: UInt32) -> Color {
|
||||
Color(
|
||||
red: Double((hex >> 16) & 0xFF) / 255,
|
||||
green: Double((hex >> 8) & 0xFF) / 255,
|
||||
blue: Double(hex & 0xFF) / 255
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// ProgressTrack.swift
|
||||
// OSGKeyboard · Shared Design System
|
||||
//
|
||||
// Shared linear progress track for consistent height, shape, and background.
|
||||
|
||||
import SwiftUI
|
||||
|
||||
public struct ProgressTrackSegment {
|
||||
let fraction: Double
|
||||
let color: Color
|
||||
|
||||
public init(fraction: Double, color: Color) {
|
||||
self.fraction = fraction
|
||||
self.color = color
|
||||
}
|
||||
}
|
||||
|
||||
public struct ProgressTrack: View {
|
||||
@Environment(\.themePalette) private var palette
|
||||
|
||||
private let segments: [ProgressTrackSegment]
|
||||
|
||||
public init(segments: [ProgressTrackSegment]) {
|
||||
self.segments = segments
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
GeometryReader { proxy in
|
||||
ZStack(alignment: .leading) {
|
||||
Capsule()
|
||||
.fill(palette.surfaceElevated.opacity(0.55))
|
||||
|
||||
HStack(spacing: 0) {
|
||||
ForEach(segments.indices, id: \.self) { index in
|
||||
Rectangle()
|
||||
.fill(segments[index].color)
|
||||
.frame(
|
||||
width: proxy.size.width * clampedFraction(
|
||||
segments[index].fraction
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
}
|
||||
.frame(height: 8)
|
||||
}
|
||||
|
||||
private func clampedFraction(_ fraction: Double) -> Double {
|
||||
min(max(fraction, 0), 1)
|
||||
}
|
||||
}
|
||||
@@ -68,7 +68,7 @@ public struct RecordButton: View {
|
||||
}
|
||||
|
||||
private var waveformColor: Color {
|
||||
Color(red: 1.0, green: 0.78, blue: 0.78)
|
||||
OSGColor.recordingWaveform
|
||||
}
|
||||
|
||||
private enum Layout {
|
||||
@@ -103,7 +103,10 @@ public struct RecordButton: View {
|
||||
.animation(Motion.soft, value: level)
|
||||
|
||||
Circle()
|
||||
.stroke(Color.white.opacity(isIdle ? 0.08 : 0.12), lineWidth: 0.5)
|
||||
.stroke(
|
||||
isIdle ? OSGColor.recordDiscHairlineIdle : OSGColor.recordDiscHairlineActive,
|
||||
lineWidth: 0.5
|
||||
)
|
||||
.frame(width: Layout.outerRing, height: Layout.outerRing)
|
||||
|
||||
ZStack {
|
||||
@@ -120,7 +123,7 @@ public struct RecordButton: View {
|
||||
Circle()
|
||||
.fill(discGradient)
|
||||
Circle()
|
||||
.stroke(Color.white.opacity(0.16), lineWidth: 1)
|
||||
.stroke(OSGColor.recordDiscOverlayStroke, lineWidth: 1)
|
||||
.blendMode(.overlay)
|
||||
}
|
||||
|
||||
@@ -129,13 +132,13 @@ public struct RecordButton: View {
|
||||
case .idleReady, .idleUnavailable:
|
||||
Image(systemName: "mic.fill")
|
||||
.font(.system(size: 36, weight: .medium))
|
||||
.foregroundStyle(.white)
|
||||
.foregroundStyle(OSGColor.fixedLightContent)
|
||||
case .recording:
|
||||
VStack(spacing: 3) {
|
||||
if let remainingSeconds {
|
||||
Text(formatRemaining(remainingSeconds))
|
||||
.font(.system(size: 22, weight: .semibold, design: .rounded))
|
||||
.foregroundStyle(.white)
|
||||
.foregroundStyle(OSGColor.fixedLightContent)
|
||||
.monospacedDigit()
|
||||
.contentTransition(.numericText())
|
||||
.offset(y: 3)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// Theme.swift
|
||||
// OSGKeyboard · Design System
|
||||
//
|
||||
// Single source of truth for colour, spacing, corner radius, typography.
|
||||
// Semantic theme roles, spacing, corner radius, and typography. Canonical
|
||||
// custom colour values live in `ColorLibrary.swift`.
|
||||
// Inspired by Dieter Rams ("less but better") and Apple Human Interface:
|
||||
// every surface has a single purpose, every token earns its place, and
|
||||
// the visual hierarchy is carried by *whitespace + one accent*, never by
|
||||
@@ -19,6 +20,7 @@ import SwiftUI
|
||||
public struct ThemePalette: Sendable, Equatable {
|
||||
public let background: Color
|
||||
public let surface: Color
|
||||
public let formSurface: Color
|
||||
public let surfaceElevated: Color
|
||||
public let surfaceMuted: Color
|
||||
|
||||
@@ -47,35 +49,36 @@ public struct ThemePalette: Sendable, Equatable {
|
||||
|
||||
public enum Palette {
|
||||
// Backgrounds
|
||||
public static let background = Color(red: 0.039, green: 0.039, blue: 0.043) // #0A0A0B
|
||||
public static let surface = Color(red: 0.094, green: 0.094, blue: 0.106) // #18181B
|
||||
public static let surfaceElevated = Color(red: 0.153, green: 0.153, blue: 0.169) // #27272A
|
||||
public static let surfaceMuted = Color(red: 0.071, green: 0.071, blue: 0.082) // #121215
|
||||
public static let background = OSGColor.darkBackground
|
||||
public static let surface = OSGColor.darkSurface
|
||||
public static let formSurface = OSGColor.darkFormSurface
|
||||
public static let surfaceElevated = OSGColor.darkSurfaceElevated
|
||||
public static let surfaceMuted = OSGColor.darkSurfaceMuted
|
||||
|
||||
// Accents
|
||||
public static let accent = Color(red: 0.227, green: 0.627, blue: 0.353) // #3AA05A
|
||||
public static let accent = OSGColor.brandAccent
|
||||
public static let accentMuted = accent.opacity(0.18)
|
||||
public static let accentGlow = accent.opacity(0.42)
|
||||
public static let aiTeal = Color(red: 0.169, green: 0.686, blue: 0.643) // #2BAFA4
|
||||
public static let aiTeal = OSGColor.aiTeal
|
||||
|
||||
// Semantic
|
||||
public static let danger = Color(red: 1.000, green: 0.271, blue: 0.227) // #FF453A
|
||||
public static let danger = OSGColor.darkDanger
|
||||
public static let success = accent // unified brand green in UI
|
||||
public static let warning = Color(red: 1.000, green: 0.749, blue: 0.094) // #FFBF18
|
||||
public static let warning = OSGColor.darkWarning
|
||||
|
||||
// Text
|
||||
public static let textPrimary = Color.white
|
||||
public static let textSecondary = Color(white: 0.7)
|
||||
public static let textTertiary = Color(white: 0.50)
|
||||
public static let textOnAccent = Color.black
|
||||
public static let textPrimary = OSGColor.darkTextPrimary
|
||||
public static let textSecondary = OSGColor.darkTextSecondary
|
||||
public static let textTertiary = OSGColor.darkTextTertiary
|
||||
public static let textOnAccent = OSGColor.fixedDarkContent
|
||||
|
||||
// Lines
|
||||
public static let divider = Color.white.opacity(0.06)
|
||||
public static let dividerStrong = Color.white.opacity(0.10)
|
||||
public static let divider = OSGColor.darkDivider
|
||||
public static let dividerStrong = OSGColor.darkDividerStrong
|
||||
|
||||
// Recording state
|
||||
public static let recordRed = Color(red: 1.000, green: 0.231, blue: 0.188) // #FF3B30
|
||||
public static let recordBlue = Color(red: 0.000, green: 0.478, blue: 1.000) // #007AFF
|
||||
public static let recordRed = OSGColor.recordRed
|
||||
public static let recordBlue = OSGColor.recordBlue
|
||||
|
||||
/// Canonical dark palette — preserves every legacy literal above so
|
||||
/// existing call sites that read `Palette.background` directly keep
|
||||
@@ -84,6 +87,7 @@ public enum Palette {
|
||||
public static let dark = ThemePalette(
|
||||
background: background,
|
||||
surface: surface,
|
||||
formSurface: formSurface,
|
||||
surfaceElevated: surfaceElevated,
|
||||
surfaceMuted: surfaceMuted,
|
||||
accent: accent,
|
||||
@@ -105,25 +109,26 @@ public enum Palette {
|
||||
|
||||
/// Light palette — warm gray backgrounds for daytime use.
|
||||
public static let light = ThemePalette(
|
||||
background: Color(red: 0.949, green: 0.945, blue: 0.933), // #F2F1EE warm gray
|
||||
surface: Color(red: 0.988, green: 0.984, blue: 0.976), // #FCFBF9
|
||||
surfaceElevated: Color(red: 0.922, green: 0.918, blue: 0.906), // #EBEAE7
|
||||
surfaceMuted: Color(red: 0.933, green: 0.929, blue: 0.918), // #EEEDE9
|
||||
accent: Color(red: 0.227, green: 0.627, blue: 0.353), // #3AA05A
|
||||
accentMuted: Color(red: 0.227, green: 0.627, blue: 0.353).opacity(0.14),
|
||||
accentGlow: Color(red: 0.227, green: 0.627, blue: 0.353).opacity(0.32),
|
||||
aiTeal: Color(red: 0.169, green: 0.686, blue: 0.643), // #2BAFA4
|
||||
danger: Color(red: 1.000, green: 0.231, blue: 0.188), // #FF3B30
|
||||
success: Color(red: 0.227, green: 0.627, blue: 0.353), // same as accent
|
||||
warning: Color(red: 1.000, green: 0.620, blue: 0.094), // #FF9E18
|
||||
textPrimary: Color(red: 0.067, green: 0.067, blue: 0.094), // #111118
|
||||
textSecondary: Color(red: 0.392, green: 0.392, blue: 0.435), // #64646F
|
||||
textTertiary: Color(red: 0.557, green: 0.557, blue: 0.604), // #8E8E9A
|
||||
textOnAccent: Color.white,
|
||||
divider: Color.black.opacity(0.06),
|
||||
dividerStrong: Color.black.opacity(0.10),
|
||||
recordRed: Color(red: 1.000, green: 0.231, blue: 0.188), // #FF3B30
|
||||
recordBlue: Color(red: 0.000, green: 0.478, blue: 1.000) // #007AFF
|
||||
background: OSGColor.lightBackground,
|
||||
surface: OSGColor.lightSurface,
|
||||
formSurface: OSGColor.lightFormSurface,
|
||||
surfaceElevated: OSGColor.lightSurfaceElevated,
|
||||
surfaceMuted: OSGColor.lightSurfaceMuted,
|
||||
accent: OSGColor.brandAccent,
|
||||
accentMuted: OSGColor.brandAccent.opacity(0.14),
|
||||
accentGlow: OSGColor.brandAccent.opacity(0.32),
|
||||
aiTeal: OSGColor.aiTeal,
|
||||
danger: OSGColor.lightDanger,
|
||||
success: OSGColor.brandAccent,
|
||||
warning: OSGColor.lightWarning,
|
||||
textPrimary: OSGColor.lightTextPrimary,
|
||||
textSecondary: OSGColor.lightTextSecondary,
|
||||
textTertiary: OSGColor.lightTextTertiary,
|
||||
textOnAccent: OSGColor.fixedLightContent,
|
||||
divider: OSGColor.lightDivider,
|
||||
dividerStrong: OSGColor.lightDividerStrong,
|
||||
recordRed: OSGColor.recordRed,
|
||||
recordBlue: OSGColor.recordBlue
|
||||
)
|
||||
}
|
||||
|
||||
@@ -235,6 +240,7 @@ private struct CardSurfaceModifier: ViewModifier {
|
||||
content
|
||||
.padding(padding)
|
||||
.background(palette.surface, in: RoundedRectangle(cornerRadius: Radius.large, style: .continuous))
|
||||
.cardElevation()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// UsageSurfaceCard.swift
|
||||
// OSGKeyboard · Shared
|
||||
//
|
||||
// Flat semantic surface used by home / dashboard stats on every platform.
|
||||
// Deliberately shadowless — hierarchy comes from the semantic surface fill.
|
||||
// Semantic surface used by home / dashboard stats on every platform.
|
||||
// Shared card elevation keeps its depth consistent with the rest of the app.
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@@ -30,5 +30,6 @@ public struct UsageSurfaceCard<Content: View>: View {
|
||||
.padding(padding)
|
||||
.background(palette.surface, in: shape)
|
||||
.clipShape(shape)
|
||||
.cardElevation()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user