Share Vault, persistence, SSH/SFTP, forwarding, and SwiftUI components across platforms. Add mobile editors, terminal input, standalone Assistant, Metrics navigation, transfers, and a unified Live Activity with host system icons. Validation: 35 iOS tests and 100 focused macOS tests pass; formatting and zh-Hans checks pass. The full macOS run reports 14 issues in the documented timing, Keychain, and popover suites. Remaining Assistant tool and CloudKit work is tracked in Memory/MOBILE_ROLLOUT.md.
92 lines
3.5 KiB
Swift
92 lines
3.5 KiB
Swift
import AppKit
|
|
import Foundation
|
|
import SwiftUI
|
|
import TermindDomain
|
|
import TermindPresentation
|
|
|
|
typealias LinuxDistributionIconEntry = TermindPresentation.LinuxDistributionIconEntry
|
|
typealias LinuxDistributionIconCatalog = TermindPresentation.LinuxDistributionIconCatalog
|
|
|
|
/// Converts a Simple Icons brand hex into a foreground that remains legible on
|
|
/// CardIcon's 16%-opacity tint surface. Only luminance moves, toward the current
|
|
/// semantic label color, and only when the original brand color misses 3:1.
|
|
@MainActor
|
|
enum LinuxDistributionBrandColor {
|
|
static func color(hex: String, colorScheme: ColorScheme) -> Color? {
|
|
guard let original = nsColor(hex: hex) else { return nil }
|
|
let appearanceName: NSAppearance.Name = colorScheme == .dark ? .darkAqua : .aqua
|
|
guard let appearance = NSAppearance(named: appearanceName) else {
|
|
return Color(nsColor: original)
|
|
}
|
|
|
|
var surface = NSColor.controlBackgroundColor
|
|
var target = NSColor.labelColor
|
|
appearance.performAsCurrentDrawingAppearance {
|
|
surface = NSColor.controlBackgroundColor.usingColorSpace(.deviceRGB) ?? surface
|
|
target = NSColor.labelColor.usingColorSpace(.deviceRGB) ?? target
|
|
}
|
|
guard let foreground = original.usingColorSpace(.deviceRGB),
|
|
let background = surface.usingColorSpace(.deviceRGB),
|
|
let label = target.usingColorSpace(.deviceRGB)
|
|
else { return Color(nsColor: original) }
|
|
|
|
var candidate = foreground
|
|
for step in 0...20 {
|
|
let amount = CGFloat(step) / 20
|
|
candidate = mixed(foreground, label, amount: amount)
|
|
let tintedSurface = mixed(background, candidate, amount: 0.16)
|
|
if contrastRatio(candidate, tintedSurface) >= 3 {
|
|
return Color(nsColor: candidate)
|
|
}
|
|
}
|
|
return Color(nsColor: candidate)
|
|
}
|
|
|
|
static func nsColor(hex: String) -> NSColor? {
|
|
guard hex.count == 6, let value = UInt32(hex, radix: 16) else { return nil }
|
|
return NSColor(
|
|
srgbRed: CGFloat((value >> 16) & 0xFF) / 255,
|
|
green: CGFloat((value >> 8) & 0xFF) / 255,
|
|
blue: CGFloat(value & 0xFF) / 255,
|
|
alpha: 1
|
|
)
|
|
}
|
|
|
|
private static func mixed(_ lhs: NSColor, _ rhs: NSColor, amount: CGFloat) -> NSColor {
|
|
let left = components(lhs)
|
|
let right = components(rhs)
|
|
return NSColor(
|
|
srgbRed: left.0 + (right.0 - left.0) * amount,
|
|
green: left.1 + (right.1 - left.1) * amount,
|
|
blue: left.2 + (right.2 - left.2) * amount,
|
|
alpha: 1
|
|
)
|
|
}
|
|
|
|
private static func contrastRatio(_ lhs: NSColor, _ rhs: NSColor) -> CGFloat {
|
|
let lighter = max(luminance(lhs), luminance(rhs))
|
|
let darker = min(luminance(lhs), luminance(rhs))
|
|
return (lighter + 0.05) / (darker + 0.05)
|
|
}
|
|
|
|
private static func luminance(_ color: NSColor) -> CGFloat {
|
|
func linear(_ value: CGFloat) -> CGFloat {
|
|
value <= 0.04045
|
|
? value / 12.92
|
|
: pow((value + 0.055) / 1.055, 2.4)
|
|
}
|
|
let value = components(color)
|
|
return 0.2126 * linear(value.0)
|
|
+ 0.7152 * linear(value.1)
|
|
+ 0.0722 * linear(value.2)
|
|
}
|
|
|
|
private static func components(_ color: NSColor) -> (CGFloat, CGFloat, CGFloat) {
|
|
var red: CGFloat = 0
|
|
var green: CGFloat = 0
|
|
var blue: CGFloat = 0
|
|
color.getRed(&red, green: &green, blue: &blue, alpha: nil)
|
|
return (red, green, blue)
|
|
}
|
|
}
|