Files
OpenStats/Packages/OpenStatsKit/Sources/OpenStatsUI/Design/Tokens.swift
T
gentpan d68fbce42a 主窗口改用液态玻璃(macOS 26),分段切换改为苹果标准样式,新增全局加载框
- 新增 Design/LiquidGlass.swift:dsGlass 修饰器(macOS 26 用 glassEffect,更早系统与离屏截图退回实色)、isInsideGlass 环境值避免玻璃叠玻璃、LoadingHUD 全局加载框
- 侧边栏是浮在窗口里的玻璃面板,选中项为滑动的淡蓝圆角底;顶栏“在菜单栏显示”一组收进玻璃胶囊
- DSButtonStyle:主按钮蓝色玻璃胶囊、次要按钮透明玻璃;IconButton / MiniIconButton 为圆形玻璃;ChipButton 为玻璃胶囊;DSToggle 在 macOS 26 用系统开关
- SegmentedControl 统一为苹果标准样式:灰色圆角底槽、等宽分段、选中段实心品牌蓝白字并滑动;PillSwitch 同样灰底
- 清理、卸载、导出诊断、修改 DNS 进行中显示全局加载框
- 没有用 safeAreaBar 让页面从顶栏下滚过:macOS 上交界处会画一条分隔线,柔和或隐藏滚动边缘都去不掉
2026-09-16 18:26:09 +05:00

226 lines
9.0 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import AppKit
import SwiftUI
/// 设计 token。界面里的颜色、字号、间距、圆角一律从这里取值。
public enum DS {}
// MARK: - 颜色
extension DS {
enum Palette {
// 品牌色(单个视图内不超过 3 种)
static let primary = Color.dynamic(light: 0x2563EB, dark: 0x3B82F6)
static let primaryHover = Color.dynamic(light: 0x1D4ED8, dark: 0x2563EB) // 加深约 10%
static let secondary = Color.dynamic(light: 0x0D9488, dark: 0x2DD4BF)
// 语义色
static let success = Color.dynamic(light: 0x16A34A, dark: 0x22C55E)
static let warning = Color.dynamic(light: 0xD97706, dark: 0xF59E0B)
static let error = Color.dynamic(light: 0xDC2626, dark: 0xF87171)
// 文字
static let textPrimary = Color.dynamic(light: 0x111827, dark: 0xF3F4F6)
static let textSecondary = Color.dynamic(light: 0x6B7280, dark: 0x9CA3AF)
static let textTertiary = Color.dynamic(light: 0x9CA3AF, dark: 0x6B7280)
static let onPrimary = Color.dynamic(light: 0xFFFFFF, dark: 0xFFFFFF)
// OpenStats 的统一配色:浅色为白底蓝色,深色为黑底蓝色。
// 窗口、侧边栏、标题栏与弹窗共用同一底色,卡片用浅灰 / 深灰实色区分层级,不加边框
static let background = Color(nsColor: NSColor.dsBackground)
static let surface = Color.dynamic(light: 0xF3F4F6, dark: 0x17181B)
/// 卡片上的控件底色(分段选中块、次要按钮、输入框)
static let elevated = Color.dynamic(light: 0xFFFFFF, dark: 0x26282D)
static let surfaceHover = Color.dynamic(light: 0x000000, lightAlpha: 0.05, dark: 0xFFFFFF, darkAlpha: 0.07)
static let border = Color.dynamic(light: 0x000000, lightAlpha: 0.07, dark: 0xFFFFFF, darkAlpha: 0.09)
static let track = Color.dynamic(light: 0x000000, lightAlpha: 0.06, dark: 0xFFFFFF, darkAlpha: 0.10)
static let neutral300 = Color.dynamic(light: 0xD1D5DB, dark: 0x3F434A)
/// 分段条里表示“剩余 / 空闲”的中性段,深到能衬白字
static let neutral500 = Color.dynamic(light: 0x6B7280, dark: 0x4B5563)
/// 浅一档的品牌蓝,用来区分同一图表里的第二类数据(如性能核)
static let primarySoft = Color.dynamic(light: 0x93C5FD, dark: 0x60A5FA)
/// 侧边栏选中项的淡蓝底
static let sidebarSelected = Color.dynamic(light: 0x2563EB, lightAlpha: 0.12, dark: 0x3B82F6, darkAlpha: 0.22)
/// 全局加载时压暗整个窗口
static let scrim = Color.dynamic(light: 0x000000, lightAlpha: 0.12, dark: 0x000000, darkAlpha: 0.40)
/// 核心类型配色:最高性能档为品牌蓝,其次浅蓝,能效核灰色
static func cluster(_ id: Int) -> Color {
switch id {
case 0: primary
case 1: primarySoft
default: neutral300
}
}
}
}
extension DS {
/// 网速配色:上传绿、下载蓝,面板与菜单栏保持一致
enum NetworkPalette {
static let upload = NSColor.dynamic(light: 0x16A34A, dark: 0x22C55E)
static let download = NSColor.dynamic(light: 0x2563EB, dark: 0x3B82F6)
}
}
extension Color {
static func dynamic(light: UInt32, lightAlpha: CGFloat = 1, dark: UInt32, darkAlpha: CGFloat = 1) -> Color {
Color(nsColor: .dynamic(light: light, lightAlpha: lightAlpha, dark: dark, darkAlpha: darkAlpha))
}
}
extension NSColor {
/// 窗口底色,AppKit 窗口背景与 SwiftUI 页面共用
static let dsBackground = NSColor.dynamic(light: 0xFFFFFF, dark: 0x0B0B0D)
static func dynamic(light: UInt32, lightAlpha: CGFloat = 1, dark: UInt32, darkAlpha: CGFloat = 1) -> NSColor {
NSColor(name: nil) { appearance in
let isDark = appearance.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua
return NSColor(hex: isDark ? dark : light, alpha: isDark ? darkAlpha : lightAlpha)
}
}
}
extension NSColor {
convenience init(hex: UInt32, alpha: CGFloat = 1) {
self.init(srgbRed: CGFloat((hex >> 16) & 0xFF) / 255,
green: CGFloat((hex >> 8) & 0xFF) / 255,
blue: CGFloat(hex & 0xFF) / 255,
alpha: alpha)
}
}
// MARK: - 字号 / 间距 / 圆角 / 尺寸
extension DS {
enum TextSize: CGFloat {
case xs = 12, sm = 14, base = 16, lg = 20, xl = 24, xxl = 32
}
/// 徽章配色:浅底、细边、深字,与 cleanip.io 的徽章一致
enum Badge {
static let successText = Color.dynamic(light: 0x15803D, dark: 0x75D99B)
static let successFill = Color.dynamic(light: 0xF0FDF4, dark: 0x142F24)
static let successBorder = Color.dynamic(light: 0xBBF7D0, dark: 0x29543C)
static let warningText = Color.dynamic(light: 0xB45309, dark: 0xF3C66C)
static let warningFill = Color.dynamic(light: 0xFFFBEB, dark: 0x342B1B)
static let warningBorder = Color.dynamic(light: 0xFDE68A, dark: 0x625030)
static let neutralText = Palette.textSecondary
static let neutralFill = Palette.elevated
static let neutralBorder = Palette.border
}
/// 0–100 分的六档色阶(F / D / C / B / A / A+),只用于评分类图表,不是界面配色
enum Grade {
static let f = Palette.error
static let d = Palette.warning
static let c = Color.dynamic(light: 0xEAB308, dark: 0xFACC15)
static let b = Color.dynamic(light: 0x84CC16, dark: 0xA3E635)
static let a = Palette.success
static let aPlus = Color.dynamic(light: 0x15803D, dark: 0x16A34A)
/// 分段上限与颜色,从低到高
static let bands: [(grade: String, upper: Int, color: Color)] = [
("F", 25, f), ("D", 50, d), ("C", 70, c), ("B", 85, b), ("A", 95, a), ("A+", 100, aPlus),
]
static func color(for score: Int) -> Color {
bands.first { score < $0.upper }?.color ?? aPlus
}
}
enum Space {
static let s1: CGFloat = 4
static let s2: CGFloat = 8
static let s3: CGFloat = 12
static let s4: CGFloat = 16
static let s6: CGFloat = 24
static let s8: CGFloat = 32
static let s12: CGFloat = 48
static let s16: CGFloat = 64
}
/// 层级:控件 < 卡片 < 面板/窗口
enum Radius {
static let sm: CGFloat = 4
static let md: CGFloat = 8
static let lg: CGFloat = 12
static let xl: CGFloat = 16 // 弹窗
}
enum Size {
static let iconInline: CGFloat = 16
static let iconStandalone: CGFloat = 20
static let controlHeight: CGFloat = 32
static let segmentHeight: CGFloat = 24
static let switchWidth: CGFloat = 40
static let switchHeight: CGFloat = 24
static let switchKnob: CGFloat = 16
static let barHeight: CGFloat = 8
static let coreBarHeight: CGFloat = 32
static let chartHeight: CGFloat = 48
static let tileChart: CGFloat = 40
static let stroke: CGFloat = 1
static let chartLine: CGFloat = 1.5
static let panelWidth: CGFloat = 640
/// 菜单栏单项详情弹窗的宽度
static let popoverWidth: CGFloat = 320
static let panelMinHeight: CGFloat = 320
static let panelGap: CGFloat = 4
/// 窗口顶栏高度,与紧凑工具栏的标题栏同高,红绿灯按钮与页面标题在同一水平线上
static let windowHeader: CGFloat = 40
/// 主窗口首次打开的最小高度(屏幕放得下时)
static let windowDefaultHeight: CGFloat = 720
/// 主窗口默认内容区宽度(侧边栏另计)
static let windowDefaultContentWidth: CGFloat = 880
static let sidebarWidth: CGFloat = 192
/// 升级提示窗口
static let updateWindowWidth: CGFloat = 480
static let updateNotesHeight: CGFloat = 192
static let valueColumn: CGFloat = 64
static let labelColumn: CGFloat = 48
}
enum Shadow {
struct Level {
let color: Color
let radius: CGFloat
let y: CGFloat
}
static let level1 = Level(color: .black.opacity(0.08), radius: 3, y: 1)
static let level2 = Level(color: .black.opacity(0.10), radius: 12, y: 4)
}
enum Motion {
static let quick = Animation.easeOut(duration: 0.15)
}
/// 温度阈值(°C),用于着色
enum Thermal {
static let warm: Double = 80
static let hot: Double = 95
static let scaleMax: Double = 110
}
}
// MARK: - 修饰器
extension View {
func dsFont(_ size: DS.TextSize, weight: Font.Weight = .regular) -> some View {
font(.system(size: size.rawValue, weight: weight))
}
}
extension Text {
func dsFont(_ size: DS.TextSize, weight: Font.Weight = .regular) -> Text {
font(.system(size: size.rawValue, weight: weight))
}
}
extension View {
func dsShadow(_ level: DS.Shadow.Level) -> some View {
shadow(color: level.color, radius: level.radius, x: 0, y: level.y)
}
}