release(ios): 0.2.2 构建 9 修复消息发送遮挡并中性化石墨灰阶
- 消息中心发送入口从被标签栏盖住的底部操作条移到导航栏右侧,发信恢复可用 - 停用态主按钮保留主色淡底与同色描边,与次级按钮拉开层级 - 消息行删除改安静线性图标,去掉一屏红块 - 缺数据读数统一为短横,卡片延迟补空格单位 - 中性灰阶改为严格 R=G=B,浅色主题不再读出冷调;图标石墨底同步去蓝 - 详情页延迟量表改用与卡片同一算法,身份信息值改为右对齐 - 标签加号按钮与输入框同高同圆角
This commit is contained in:
@@ -2,18 +2,18 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">
|
||||
<defs>
|
||||
<linearGradient id="field" x1="0.06" y1="0" x2="0.94" y2="1">
|
||||
<stop offset="0" stop-color="#202021"/>
|
||||
<stop offset="0.55" stop-color="#131314"/>
|
||||
<stop offset="1" stop-color="#0A0A0B"/>
|
||||
<stop offset="0" stop-color="#202020"/>
|
||||
<stop offset="0.55" stop-color="#131313"/>
|
||||
<stop offset="1" stop-color="#0A0A0A"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="brass" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stop-color="#FADD98"/>
|
||||
<stop offset="0.46" stop-color="#EEBF64"/>
|
||||
<stop offset="1" stop-color="#C78D2F"/>
|
||||
<stop offset="0" stop-color="#FCE5AA"/>
|
||||
<stop offset="0.46" stop-color="#F0C76F"/>
|
||||
<stop offset="1" stop-color="#D39B3D"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="slot" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#101011"/>
|
||||
<stop offset="1" stop-color="#202021"/>
|
||||
<stop offset="0" stop-color="#101010"/>
|
||||
<stop offset="1" stop-color="#202020"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
@@ -133,9 +133,10 @@ func directionalRim(_ path: CGPath, _ rect: CGRect, _ width: CGFloat, _ stops: [
|
||||
fillLinear(
|
||||
rectPath(CGRect(x: 0, y: 0, width: side, height: side)),
|
||||
[
|
||||
(0.00, (32, 32, 33, 255)),
|
||||
(0.55, (19, 19, 20, 255)),
|
||||
(1.00, (10, 10, 11, 255)),
|
||||
// 石墨底严格等色。原先 B 比 R 高 1 级,缩到 60pt 后边缘像素仍会落在蓝色波段。
|
||||
(0.00, (32, 32, 32, 255)),
|
||||
(0.55, (19, 19, 19, 255)),
|
||||
(1.00, (10, 10, 10, 255)),
|
||||
] as [Stop],
|
||||
CGPoint(x: side * 0.06, y: 0),
|
||||
CGPoint(x: side * 0.94, y: side)
|
||||
@@ -169,9 +170,10 @@ ctx.restoreGState()
|
||||
fillLinear(
|
||||
plate,
|
||||
[
|
||||
(0.00, (250, 221, 152, 255)),
|
||||
(0.46, (238, 191, 100, 255)),
|
||||
(1.00, (199, 141, 47, 255)),
|
||||
// 金色往香槟靠,收掉底部的芥末/棕调:小尺寸下这段最容易被读成"脏黄"。
|
||||
(0.00, (252, 229, 170, 255)),
|
||||
(0.46, (240, 199, 111, 255)),
|
||||
(1.00, (211, 155, 61, 255)),
|
||||
] as [Stop],
|
||||
CGPoint(x: plateRect.minX, y: plateRect.minY),
|
||||
CGPoint(x: plateRect.maxX, y: plateRect.maxY)
|
||||
@@ -191,6 +193,8 @@ let slotWidth = envDouble("ICON_SLOT_W", 92)
|
||||
let slotGap = envDouble("ICON_SLOT_GAP", 54)
|
||||
let slotBottom = envDouble("ICON_SLOT_BOTTOM", 640)
|
||||
let slotHeights: [CGFloat] = [136, 232, 328]
|
||||
// 说明:以上默认值等价于 plate 相对比例(宽 .155 / 间距 .091 / 底边内缩 .265),
|
||||
// 改 plate 尺寸时按同一比例缩放才不会让槽和卡片脱节。
|
||||
let slotsWidth = CGFloat(slotHeights.count) * slotWidth + CGFloat(slotHeights.count - 1) * slotGap
|
||||
let slotsStartX = (side - slotsWidth) / 2
|
||||
|
||||
@@ -207,8 +211,8 @@ for (index, height) in slotHeights.enumerated() {
|
||||
fillLinear(
|
||||
path,
|
||||
[
|
||||
(0.00, (16, 16, 17, 255)),
|
||||
(1.00, (32, 32, 33, 255)),
|
||||
(0.00, (16, 16, 16, 255)),
|
||||
(1.00, (32, 32, 32, 255)),
|
||||
] as [Stop],
|
||||
CGPoint(x: rect.minX, y: rect.minY),
|
||||
CGPoint(x: rect.minX, y: rect.maxY)
|
||||
@@ -217,9 +221,9 @@ for (index, height) in slotHeights.enumerated() {
|
||||
directionalRim(
|
||||
path, rect, radius * 0.30,
|
||||
[
|
||||
(0.00, (112, 74, 16, 100)),
|
||||
(0.52, (112, 74, 16, 18)),
|
||||
(1.00, (255, 236, 190, 60)),
|
||||
(0.00, (120, 82, 24, 96)),
|
||||
(0.52, (120, 82, 24, 16)),
|
||||
(1.00, (255, 240, 202, 64)),
|
||||
] as [Stop]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,5 +2,5 @@ public enum AppMetadata {
|
||||
public static let name = "MultiSimAdmin"
|
||||
public static let bundleIdentifier = "fun.chick.multi-simadmin"
|
||||
public static let marketingVersion = "0.2.2"
|
||||
public static let buildVersion = "8"
|
||||
public static let buildVersion = "9"
|
||||
}
|
||||
|
||||
@@ -237,7 +237,9 @@ struct InstanceDetailView: View {
|
||||
section: "connectivity",
|
||||
keys: ["latency_ms", "latency"],
|
||||
format: loading ? nil : { "\(Int($0.rounded())) ms" },
|
||||
progress: loading ? nil : { _ in 1 },
|
||||
// 进度条读的是"链路质量",和节点卡片同一把尺子(1000ms 归零),
|
||||
// 之前恒为 1,满格黑条压在 38ms 上会被读成"延迟占满了什么"。
|
||||
progress: loading ? nil : { max(0.04, min(1, 1 - $0 / 1000)) },
|
||||
tint: loading ? nil : { latencyTint($0) }
|
||||
)
|
||||
let cpu = summaryValue(
|
||||
@@ -562,18 +564,22 @@ struct InstanceDetailView: View {
|
||||
} else if items.isEmpty == false {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
ForEach(items, id: \.label) { item in
|
||||
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
||||
// 值一律右对齐,和下方分区卡片的键值行同一节奏;
|
||||
// 之前左边缩进 52pt、值顶在中间,同屏两种排法读起来像两张不同的表。
|
||||
HStack(alignment: .firstTextBaseline, spacing: 12) {
|
||||
Text(item.label)
|
||||
.font(.caption2.weight(.semibold))
|
||||
.foregroundStyle(AITheme.muted)
|
||||
.frame(width: 52, alignment: .leading)
|
||||
.fixedSize(horizontal: true, vertical: false)
|
||||
Spacer(minLength: 12)
|
||||
Text(item.value)
|
||||
.font(.caption.weight(.medium))
|
||||
.monospacedDigit()
|
||||
.foregroundStyle(AITheme.ink)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
Spacer(minLength: 0)
|
||||
.multilineTextAlignment(.trailing)
|
||||
.layoutPriority(1)
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
|
||||
@@ -180,6 +180,9 @@ struct FleetView: View {
|
||||
}
|
||||
|
||||
private extension FleetView {
|
||||
/// 概览读数缺数据时只画一根短横:「不可用」在四列窄格里既占位又容易被读成故障结论。
|
||||
static let noDataText = "—"
|
||||
|
||||
private static let relativeFormatter: RelativeDateTimeFormatter = {
|
||||
let formatter = RelativeDateTimeFormatter()
|
||||
formatter.unitsStyle = .abbreviated
|
||||
@@ -361,7 +364,7 @@ private extension FleetView {
|
||||
)
|
||||
FleetHeadlineStat(
|
||||
title: "异常节点",
|
||||
value: settledCount == 0 ? "--" : "\(abnormalCount)",
|
||||
value: settledCount == 0 ? Self.noDataText : "\(abnormalCount)",
|
||||
detail: abnormalDetail,
|
||||
tint: abnormalTint
|
||||
)
|
||||
@@ -666,7 +669,7 @@ private extension FleetView {
|
||||
private var hasSettledResults: Bool { summaryRows.isEmpty == false && settledCount > 0 }
|
||||
|
||||
private var onlineRateValue: String {
|
||||
guard hasSettledResults else { return "--" }
|
||||
guard hasSettledResults else { return Self.noDataText }
|
||||
return "\(Int((Double(onlineCount) / Double(summaryRows.count) * 100).rounded()))%"
|
||||
}
|
||||
|
||||
@@ -749,10 +752,10 @@ private extension FleetView {
|
||||
|
||||
private var storageSummary: String {
|
||||
let pairs = storagePairs
|
||||
guard pairs.isEmpty == false else { return "不可用" }
|
||||
guard pairs.isEmpty == false else { return Self.noDataText }
|
||||
let used = pairs.map { max(0, $0.total - $0.available) }.reduce(0, +)
|
||||
let total = pairs.map(\.total).reduce(0, +)
|
||||
guard total > 0 else { return "不可用" }
|
||||
guard total > 0 else { return Self.noDataText }
|
||||
return "\(Int((Double(used) / Double(total) * 100).rounded()))% 已用"
|
||||
}
|
||||
|
||||
@@ -777,7 +780,7 @@ private extension FleetView {
|
||||
|
||||
private var trafficSummary: String {
|
||||
let values = summaryRows.compactMap(\.downloadRateBytesPerSecond) + summaryRows.compactMap(\.uploadRateBytesPerSecond)
|
||||
guard values.isEmpty == false else { return "不可用" }
|
||||
guard values.isEmpty == false else { return Self.noDataText }
|
||||
return ByteCountFormatter.string(fromByteCount: Int64(values.reduce(0, +)), countStyle: .decimal) + "/s"
|
||||
}
|
||||
|
||||
@@ -793,7 +796,7 @@ private extension FleetView {
|
||||
}
|
||||
|
||||
private func formatAverage(_ values: [Double], transform: (Double) -> String) -> String {
|
||||
guard values.isEmpty == false else { return "不可用" }
|
||||
guard values.isEmpty == false else { return Self.noDataText }
|
||||
let average = values.reduce(0, +) / Double(values.count)
|
||||
return transform(average)
|
||||
}
|
||||
@@ -927,25 +930,25 @@ private extension FleetView {
|
||||
[
|
||||
FleetGauge(
|
||||
label: "信号",
|
||||
value: row.signalPercent.map { "\($0)%" } ?? "--",
|
||||
value: row.signalPercent.map { "\($0)%" } ?? Self.noDataText,
|
||||
ratio: row.signalPercent.map { Double($0) / 100 },
|
||||
tint: row.signalPercent.flatMap(signalColor) ?? AITheme.muted
|
||||
),
|
||||
FleetGauge(
|
||||
label: "延迟",
|
||||
value: row.latencyMilliseconds.map { "\($0)ms" } ?? "--",
|
||||
value: row.latencyMilliseconds.map { "\($0) ms" } ?? Self.noDataText,
|
||||
ratio: row.latencyMilliseconds.map { max(0, 1 - Double($0) / 1000) },
|
||||
tint: row.latencyMilliseconds.flatMap(latencyColor) ?? AITheme.muted
|
||||
),
|
||||
FleetGauge(
|
||||
label: "CPU",
|
||||
value: row.cpuPercent.map(Self.percentText) ?? "--",
|
||||
value: row.cpuPercent.map(Self.percentText) ?? Self.noDataText,
|
||||
ratio: row.cpuPercent.map { $0 / 100 },
|
||||
tint: row.cpuPercent.flatMap(resourceColor) ?? AITheme.muted
|
||||
),
|
||||
FleetGauge(
|
||||
label: "内存",
|
||||
value: row.memoryPercent.map(Self.percentText) ?? "--",
|
||||
value: row.memoryPercent.map(Self.percentText) ?? Self.noDataText,
|
||||
ratio: row.memoryPercent.map { $0 / 100 },
|
||||
tint: row.memoryPercent.flatMap(resourceColor) ?? AITheme.muted
|
||||
),
|
||||
@@ -993,7 +996,7 @@ private extension FleetView {
|
||||
|
||||
private static func cpuValue(percent: Double?, cores: Int?) -> String? {
|
||||
guard percent != nil || cores != nil else { return nil }
|
||||
let usage = percent.map(Self.percentText) ?? "--"
|
||||
let usage = percent.map(Self.percentText) ?? FleetView.noDataText
|
||||
guard let cores, cores > 0 else { return usage }
|
||||
return "\(usage) · \(cores) 核"
|
||||
}
|
||||
@@ -1296,7 +1299,7 @@ extension FleetView {
|
||||
|
||||
// 没有数据时用弱色占位,读起来不会像"0%"那样的假结论。
|
||||
var isPlaceholder: Bool {
|
||||
value == "--" || value == "不可用" || value == "暂无"
|
||||
value == "--" || value == "—" || value == "不可用" || value == "暂无"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1546,7 +1549,7 @@ extension FleetView {
|
||||
.font(.caption2)
|
||||
.foregroundStyle(AITheme.faint)
|
||||
.lineLimit(1)
|
||||
Text(value ?? "--")
|
||||
Text(value ?? FleetView.noDataText)
|
||||
.font(.footnote.weight(.semibold))
|
||||
.monospacedDigit()
|
||||
.foregroundStyle(isAlert ? tint : AITheme.ink)
|
||||
|
||||
@@ -22,16 +22,17 @@ struct TagEditorView: View {
|
||||
commit(draft)
|
||||
} label: {
|
||||
Image(systemName: "plus")
|
||||
.font(.system(size: 15, weight: .semibold))
|
||||
.frame(width: 44, height: 44)
|
||||
// 加号按钮和左边的输入框同一高度、同一圆角,两个控件才读得出是一组。
|
||||
.font(.system(size: 16, weight: .semibold))
|
||||
.frame(width: AITheme.inputHeight, height: AITheme.inputHeight)
|
||||
.foregroundStyle(canAdd ? AITheme.primary : AITheme.muted)
|
||||
.background(canAdd ? AITheme.primary.opacity(0.10) : AITheme.control)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
|
||||
.clipShape(RoundedRectangle(cornerRadius: AITheme.inputRadius, style: .continuous))
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
||||
RoundedRectangle(cornerRadius: AITheme.inputRadius, style: .continuous)
|
||||
.strokeBorder(canAdd ? AITheme.primary.opacity(0.40) : AITheme.line)
|
||||
}
|
||||
.contentShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
|
||||
.contentShape(RoundedRectangle(cornerRadius: AITheme.inputRadius, style: .continuous))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.disabled(canAdd == false)
|
||||
|
||||
@@ -237,7 +237,7 @@ struct LogsView: View {
|
||||
status: entry.outcome.label,
|
||||
statusTint: outcomeTint(entry.outcome),
|
||||
details: [
|
||||
"HTTP \(entry.httpStatus.map(String.init) ?? "--") · \(entry.durationMs) ms",
|
||||
"HTTP \(entry.httpStatus.map(String.init) ?? "—") · \(entry.durationMs) ms",
|
||||
entry.errorCode ?? "无错误码",
|
||||
]
|
||||
)
|
||||
|
||||
@@ -46,22 +46,21 @@ struct MessagesView: View {
|
||||
.padding(16)
|
||||
.padding(.bottom, 8)
|
||||
}
|
||||
.aiBottomActionBar {
|
||||
Button {
|
||||
model.showingSendForm = true
|
||||
} label: {
|
||||
Label("发送消息", systemImage: "paperplane")
|
||||
}
|
||||
.buttonStyle(AIButtonStyle(kind: .primary, size: .regular, fillsWidth: true))
|
||||
}
|
||||
.background(AITheme.canvas.ignoresSafeArea())
|
||||
.navigationTitle("消息中心")
|
||||
.toolbar {
|
||||
// 发送入口和节点页的「添加实例」保持一致:放在导航栏右侧,
|
||||
// 底部固定条在 TabView 里会被自定义标签栏盖住,之前整条 CTA 是不可达的。
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
AIIconButton("刷新", systemImage: "arrow.clockwise") {
|
||||
Task { await model.refresh() }
|
||||
HStack(spacing: 8) {
|
||||
AIIconButton("刷新", systemImage: "arrow.clockwise") {
|
||||
Task { await model.refresh() }
|
||||
}
|
||||
.disabled(model.isLoading)
|
||||
AIIconButton("发送消息", systemImage: "paperplane") {
|
||||
model.showingSendForm = true
|
||||
}
|
||||
}
|
||||
.disabled(model.isLoading)
|
||||
}
|
||||
}
|
||||
.confirmationDialog(
|
||||
@@ -164,7 +163,7 @@ struct MessagesView: View {
|
||||
.lineLimit(1)
|
||||
}
|
||||
Spacer(minLength: 8)
|
||||
AISymbolAction("删除这条消息", systemImage: "trash", tint: AITheme.danger) {
|
||||
AISymbolAction("删除这条消息", systemImage: "trash", tint: AITheme.danger, quiet: true) {
|
||||
pendingDeleteMessage = message
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ struct SettingsRuntime: Equatable {
|
||||
final class SettingsViewModel: ObservableObject {
|
||||
@Published private(set) var runtime: SettingsRuntime?
|
||||
@Published private(set) var isLoading = false
|
||||
@Published var storageLabel = "--"
|
||||
@Published var storageLabel = "—"
|
||||
@Published var notice: String?
|
||||
@Published var errorMessage: String?
|
||||
|
||||
@@ -61,7 +61,7 @@ final class SettingsViewModel: ObservableObject {
|
||||
let connectionCount = try await store.connectionLogs().count
|
||||
storageLabel = "\(runtimeCount) 条运行日志 · \(connectionCount) 条连接日志"
|
||||
} catch {
|
||||
storageLabel = "--"
|
||||
storageLabel = "—"
|
||||
errorMessage = "无法读取本地数据状态。"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 812 KiB After Width: | Height: | Size: 809 KiB |
@@ -10,7 +10,7 @@
|
||||
<key>CFBundleName</key><string>MultiSimAdmin</string>
|
||||
<key>CFBundlePackageType</key><string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key><string>0.2.2</string>
|
||||
<key>CFBundleVersion</key><string>8</string>
|
||||
<key>CFBundleVersion</key><string>9</string>
|
||||
<key>LSRequiresIPhoneOS</key><true/>
|
||||
<key>MinimumOSVersion</key><string>16.0</string>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
|
||||
@@ -21,10 +21,11 @@ public struct AIButtonStyle: ButtonStyle {
|
||||
}
|
||||
|
||||
func backgroundColor(isEnabled: Bool) -> Color {
|
||||
switch self {
|
||||
case .primary, .secondary, .danger:
|
||||
return isEnabled ? kindBackground : AITheme.control
|
||||
}
|
||||
if isEnabled { return kindBackground }
|
||||
// 禁用态:实心主色按钮保留主色淡版,其余退回中性灰。
|
||||
return self == .primary
|
||||
? AITheme.primary.opacity(AITheme.disabledFillOpacity)
|
||||
: AITheme.control
|
||||
}
|
||||
|
||||
private var kindBackground: Color {
|
||||
@@ -34,6 +35,13 @@ public struct AIButtonStyle: ButtonStyle {
|
||||
case .danger: return AITheme.danger
|
||||
}
|
||||
}
|
||||
|
||||
/// 禁用态下主按钮必须还能看出"这是主行动"。
|
||||
/// 之前三种 kind 禁用后都塌成同一块 control 灰,表单里"取消"和"保存"长得一模一样,
|
||||
/// 用户只能靠文字猜哪个是提交。这里让实心主色按钮改用同色相淡填充 + 描边,灰底次按钮不套。
|
||||
var usesTintedDisabledTreatment: Bool {
|
||||
self == .primary
|
||||
}
|
||||
}
|
||||
|
||||
public enum Size: Hashable {
|
||||
@@ -74,6 +82,8 @@ public struct AIButtonStyle: ButtonStyle {
|
||||
case .toolbarInline: AITheme.toolbarButtonHeight
|
||||
}
|
||||
|
||||
let isTintedDisabled = kind.usesTintedDisabledTreatment && !isEnabled
|
||||
|
||||
configuration.label
|
||||
.labelStyle(.titleAndIcon)
|
||||
.font(
|
||||
@@ -94,7 +104,7 @@ public struct AIButtonStyle: ButtonStyle {
|
||||
AnyShapeStyle(kind.backgroundColor(isEnabled: isEnabled))
|
||||
)
|
||||
.shadow(
|
||||
color: (kind == .secondary || !isEnabled) ? .clear : .black.opacity(0.10),
|
||||
color: (kind == .secondary || isTintedDisabled) ? .clear : .black.opacity(0.10),
|
||||
radius: 12,
|
||||
x: 0,
|
||||
y: 5
|
||||
@@ -111,9 +121,11 @@ public struct AIButtonStyle: ButtonStyle {
|
||||
style: .continuous
|
||||
)
|
||||
.strokeBorder(
|
||||
size == .toolbarInline && kind == .primary
|
||||
? AITheme.primary.opacity(0.28)
|
||||
: (kind == .secondary || !isEnabled ? AITheme.line : .clear)
|
||||
isTintedDisabled
|
||||
? AITheme.primary.opacity(AITheme.disabledStrokeOpacity)
|
||||
: size == .toolbarInline && kind == .primary
|
||||
? AITheme.primary.opacity(0.28)
|
||||
: (kind == .secondary || !isEnabled ? AITheme.line : .clear)
|
||||
)
|
||||
}
|
||||
.scaleEffect(configuration.isPressed ? Self.pressedScale : 1)
|
||||
|
||||
@@ -820,17 +820,22 @@ public struct AISymbolAction: View {
|
||||
private let title: String
|
||||
private let systemImage: String
|
||||
private let tint: Color
|
||||
/// 安静样式:只留着色图标与点击区,不再铺色块。
|
||||
/// 卡片列表里每行都放一块彩色方块会把整页染成"警告",删除这类低频动作更适合安静样式。
|
||||
private let isQuiet: Bool
|
||||
private let action: () -> Void
|
||||
|
||||
public init(
|
||||
_ title: String,
|
||||
systemImage: String,
|
||||
tint: Color = AITheme.muted,
|
||||
quiet: Bool = false,
|
||||
action: @escaping () -> Void
|
||||
) {
|
||||
self.title = title
|
||||
self.systemImage = systemImage
|
||||
self.tint = tint
|
||||
self.isQuiet = quiet
|
||||
self.action = action
|
||||
}
|
||||
|
||||
@@ -842,11 +847,13 @@ public struct AISymbolAction: View {
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
.foregroundStyle(tint)
|
||||
.frame(width: AITheme.iconButtonSize, height: AITheme.iconButtonSize)
|
||||
.background(tint.opacity(0.10))
|
||||
.background(isQuiet ? Color.clear : tint.opacity(0.10))
|
||||
.clipShape(RoundedRectangle(cornerRadius: AITheme.iconButtonRadius, style: .continuous))
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: AITheme.iconButtonRadius, style: .continuous)
|
||||
.strokeBorder(tint.opacity(0.18))
|
||||
if isQuiet == false {
|
||||
RoundedRectangle(cornerRadius: AITheme.iconButtonRadius, style: .continuous)
|
||||
.strokeBorder(tint.opacity(0.18))
|
||||
}
|
||||
}
|
||||
.frame(width: 44, height: 44)
|
||||
.contentShape(Rectangle())
|
||||
|
||||
@@ -28,76 +28,77 @@ private enum AIPalette {
|
||||
dynamicColor(light: light, dark: dark)
|
||||
}
|
||||
|
||||
// 石墨灰阶:所有中性色 R≈G≈B,不带蓝调;主色让位给黑/白,颜色只用来表达状态。
|
||||
static let canvas = color(light: rgba(244, 245, 245, 1), dark: rgba(8, 9, 9, 1))
|
||||
static let surface = color(light: rgba(255, 255, 255, 1), dark: rgba(19, 21, 21, 1))
|
||||
static let surfaceRaised = color(light: rgba(255, 255, 255, 1), dark: rgba(27, 29, 30, 1))
|
||||
static let control = color(light: rgba(238, 239, 239, 1), dark: rgba(36, 39, 40, 1))
|
||||
static let tile = color(light: rgba(246, 247, 247, 1), dark: rgba(255, 255, 255, 0.055))
|
||||
static let tileLine = color(light: rgba(231, 233, 233, 1), dark: rgba(255, 255, 255, 0.09))
|
||||
static let chrome = color(light: rgba(252, 252, 252, 0.92), dark: rgba(11, 12, 12, 0.88))
|
||||
static let ink = color(light: rgba(17, 19, 20, 1), dark: rgba(240, 242, 242, 1))
|
||||
static let muted = color(light: rgba(88, 94, 97, 1), dark: rgba(158, 165, 168, 1))
|
||||
static let faint = color(light: rgba(126, 132, 135, 1), dark: rgba(112, 118, 121, 1))
|
||||
static let line = color(light: rgba(223, 226, 226, 1), dark: rgba(255, 255, 255, 0.11))
|
||||
static let brand = color(light: rgba(18, 20, 21, 1), dark: rgba(8, 9, 9, 1))
|
||||
static let primary = color(light: rgba(21, 24, 25, 1), dark: rgba(238, 241, 241, 1))
|
||||
static let onPrimary = color(light: rgba(255, 255, 255, 1), dark: rgba(12, 14, 14, 1))
|
||||
// 中性石墨灰阶:所有中性色严格 R=G=B,既不冷到读出蓝,也不暖到读出米色;主色让位给
|
||||
// 黑/白,颜色只用来表达状态。原先 B 比 R 高 3~10 级,浅色下大面积文字与描边会蒙一层冷调。
|
||||
static let canvas = color(light: rgba(245, 245, 245, 1), dark: rgba(10, 10, 10, 1))
|
||||
static let surface = color(light: rgba(255, 255, 255, 1), dark: rgba(21, 21, 21, 1))
|
||||
static let surfaceRaised = color(light: rgba(255, 255, 255, 1), dark: rgba(29, 29, 29, 1))
|
||||
static let control = color(light: rgba(239, 239, 239, 1), dark: rgba(39, 39, 39, 1))
|
||||
static let tile = color(light: rgba(247, 247, 247, 1), dark: rgba(255, 255, 255, 0.055))
|
||||
static let tileLine = color(light: rgba(232, 232, 232, 1), dark: rgba(255, 255, 255, 0.09))
|
||||
static let chrome = color(light: rgba(252, 252, 252, 0.92), dark: rgba(13, 13, 13, 0.88))
|
||||
static let ink = color(light: rgba(18, 18, 18, 1), dark: rgba(242, 242, 242, 1))
|
||||
static let muted = color(light: rgba(92, 92, 92, 1), dark: rgba(164, 164, 164, 1))
|
||||
static let faint = color(light: rgba(130, 130, 130, 1), dark: rgba(118, 118, 118, 1))
|
||||
static let line = color(light: rgba(226, 226, 226, 1), dark: rgba(255, 255, 255, 0.11))
|
||||
static let brand = color(light: rgba(18, 18, 18, 1), dark: rgba(9, 9, 9, 1))
|
||||
static let primary = color(light: rgba(23, 23, 23, 1), dark: rgba(240, 240, 240, 1))
|
||||
static let onPrimary = color(light: rgba(255, 255, 255, 1), dark: rgba(13, 13, 13, 1))
|
||||
static let danger = color(light: rgba(193, 46, 40, 1), dark: rgba(247, 113, 105, 1))
|
||||
static let onDanger = color(light: rgba(255, 255, 255, 1), dark: rgba(24, 8, 7, 1))
|
||||
static let warning = color(light: rgba(170, 92, 15, 1), dark: rgba(233, 166, 60, 1))
|
||||
static let success = color(light: rgba(16, 122, 86, 1), dark: rgba(66, 199, 132, 1))
|
||||
static let segmentTrack = color(light: rgba(234, 236, 236, 1), dark: rgba(26, 29, 29, 1))
|
||||
static let segmentSelected = color(light: rgba(255, 255, 255, 1), dark: rgba(54, 58, 59, 1))
|
||||
static let cardShadow = color(light: rgba(17, 22, 25, 0.06), dark: rgba(0, 0, 0, 0.60))
|
||||
static let segmentTrack = color(light: rgba(235, 235, 235, 1), dark: rgba(28, 28, 28, 1))
|
||||
static let segmentSelected = color(light: rgba(255, 255, 255, 1), dark: rgba(58, 58, 58, 1))
|
||||
static let cardShadow = color(light: rgba(18, 18, 18, 0.06), dark: rgba(0, 0, 0, 0.60))
|
||||
#elseif canImport(AppKit)
|
||||
static let canvas = Color(red: 0.957, green: 0.961, blue: 0.961)
|
||||
static let canvas = Color(red: 0.961, green: 0.961, blue: 0.961)
|
||||
static let surface = Color.white
|
||||
static let control = Color(red: 0.933, green: 0.937, blue: 0.937)
|
||||
static let ink = Color(red: 0.067, green: 0.075, blue: 0.078)
|
||||
static let muted = Color(red: 0.345, green: 0.369, blue: 0.380)
|
||||
static let line = Color(red: 0.875, green: 0.886, blue: 0.886)
|
||||
static let brand = Color(red: 0.071, green: 0.078, blue: 0.082)
|
||||
static let control = Color(red: 0.937, green: 0.937, blue: 0.937)
|
||||
static let ink = Color(red: 0.071, green: 0.071, blue: 0.071)
|
||||
static let muted = Color(red: 0.361, green: 0.361, blue: 0.361)
|
||||
static let line = Color(red: 0.886, green: 0.886, blue: 0.886)
|
||||
static let brand = Color(red: 0.071, green: 0.071, blue: 0.071)
|
||||
static let surfaceRaised = Color.white
|
||||
static let chrome = Color.white.opacity(0.86)
|
||||
static let onPrimary = Color.white
|
||||
static let onDanger = Color.white
|
||||
static let primary = Color(red: 0.082, green: 0.094, blue: 0.098)
|
||||
static let primary = Color(red: 0.090, green: 0.090, blue: 0.090)
|
||||
static let danger = Color(red: 0.757, green: 0.180, blue: 0.157)
|
||||
static let warning = Color(red: 0.667, green: 0.361, blue: 0.059)
|
||||
static let success = Color(red: 0.063, green: 0.478, blue: 0.337)
|
||||
static let tile = Color(red: 0.965, green: 0.969, blue: 0.969)
|
||||
static let tileLine = Color(red: 0.906, green: 0.914, blue: 0.914)
|
||||
static let faint = Color(red: 0.494, green: 0.518, blue: 0.529)
|
||||
static let segmentTrack = Color(red: 0.918, green: 0.925, blue: 0.925)
|
||||
static let tile = Color(red: 0.969, green: 0.969, blue: 0.969)
|
||||
static let tileLine = Color(red: 0.910, green: 0.910, blue: 0.910)
|
||||
static let faint = Color(red: 0.510, green: 0.510, blue: 0.510)
|
||||
static let segmentTrack = Color(red: 0.922, green: 0.922, blue: 0.922)
|
||||
static let segmentSelected = Color.white
|
||||
static let cardShadow = Color(red: 0.067, green: 0.086, blue: 0.098).opacity(0.06)
|
||||
static let cardShadow = Color(red: 0.071, green: 0.071, blue: 0.071).opacity(0.06)
|
||||
#else
|
||||
private static func color(_ red: CGFloat, _ green: CGFloat, _ blue: CGFloat) -> Color {
|
||||
Color(red: red / 255, green: green / 255, blue: blue / 255)
|
||||
}
|
||||
|
||||
static let canvas = color(243, 245, 248)
|
||||
static let canvas = color(245, 245, 245)
|
||||
static let surface = color(255, 255, 255)
|
||||
static let control = color(247, 249, 251)
|
||||
static let ink = color(28, 39, 56)
|
||||
static let muted = color(111, 124, 143)
|
||||
static let line = color(224, 229, 235)
|
||||
static let brand = color(19, 30, 48)
|
||||
static let surfaceRaised = color(251, 252, 253)
|
||||
static let control = color(239, 239, 239)
|
||||
static let ink = color(18, 18, 18)
|
||||
static let muted = color(92, 92, 92)
|
||||
static let line = color(226, 226, 226)
|
||||
static let brand = color(18, 18, 18)
|
||||
static let surfaceRaised = color(255, 255, 255)
|
||||
static let chrome = color(255, 255, 255, 0.82)
|
||||
static let onPrimary = color(255, 255, 255)
|
||||
static let onDanger = color(255, 255, 255)
|
||||
static let primary = color(17, 20, 25)
|
||||
static let primary = color(23, 23, 23)
|
||||
static let danger = color(190, 65, 57)
|
||||
static let warning = color(170, 111, 23)
|
||||
static let success = color(50, 150, 101)
|
||||
static let tile = color(247, 249, 252)
|
||||
static let tileLine = color(233, 239, 247)
|
||||
static let faint = color(141, 152, 170)
|
||||
static let segmentTrack = color(236, 240, 246)
|
||||
static let tile = color(247, 247, 247)
|
||||
static let tileLine = color(232, 232, 232)
|
||||
static let faint = color(130, 130, 130)
|
||||
static let segmentTrack = color(235, 235, 235)
|
||||
static let segmentSelected = color(255, 255, 255)
|
||||
static let cardShadow = color(15, 30, 60)
|
||||
static let cardShadow = color(18, 18, 18)
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -125,6 +126,9 @@ public enum AITheme {
|
||||
public static let cardShadow = AIPalette.cardShadow
|
||||
|
||||
public static let buttonHeight: CGFloat = 48
|
||||
// 停用态主按钮保留一层主色淡底,避免和次级按钮长得一模一样,层级读不出来。
|
||||
public static let disabledFillOpacity: Double = 0.14
|
||||
public static let disabledStrokeOpacity: Double = 0.22
|
||||
public static let compactButtonHeight: CGFloat = 44
|
||||
public static let largeButtonHeight: CGFloat = 56
|
||||
public static let toolbarButtonHeight: CGFloat = 44
|
||||
|
||||
@@ -77,7 +77,20 @@ final class AIThemeTests: XCTestCase {
|
||||
]
|
||||
let disabledColors = disabledKinds.map { $0.backgroundColor(isEnabled: false) }
|
||||
|
||||
XCTAssertEqual(Set(disabledColors), [AITheme.control])
|
||||
// 停用态仍然要保住按钮层级:主按钮留一层主色淡底,次级/危险退回中性 control。
|
||||
XCTAssertEqual(disabledColors.count, 3)
|
||||
XCTAssertEqual(disabledColors[1], AITheme.control)
|
||||
XCTAssertEqual(disabledColors[2], AITheme.control)
|
||||
XCTAssertTrue(disabledColors[0] != AITheme.control)
|
||||
// 只有实心主色按钮需要"同色相淡填充"处理,灰底次按钮不能套,否则两者又会长一样。
|
||||
XCTAssertEqual(
|
||||
[
|
||||
AIButtonStyle.Kind.primary,
|
||||
.secondary,
|
||||
.danger,
|
||||
].map(\.usesTintedDisabledTreatment),
|
||||
[true, false, false]
|
||||
)
|
||||
XCTAssertEqual(
|
||||
disabledKinds.map { $0.foregroundColor(isEnabled: false) },
|
||||
[
|
||||
|
||||
@@ -7,6 +7,6 @@ final class AppMetadataTests: XCTestCase {
|
||||
XCTAssertEqual(AppMetadata.name, "MultiSimAdmin")
|
||||
XCTAssertEqual(AppMetadata.bundleIdentifier, "fun.chick.multi-simadmin")
|
||||
XCTAssertEqual(AppMetadata.marketingVersion, "0.2.2")
|
||||
XCTAssertEqual(AppMetadata.buildVersion, "8")
|
||||
XCTAssertEqual(AppMetadata.buildVersion, "9")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ final class SettingsViewModelTests: XCTestCase {
|
||||
|
||||
XCTAssertEqual(model.runtime?.appName, "MultiSimAdmin")
|
||||
XCTAssertEqual(model.runtime?.marketingVersion, "0.2.2")
|
||||
XCTAssertEqual(model.runtime?.buildVersion, "8")
|
||||
XCTAssertEqual(model.runtime?.buildVersion, "9")
|
||||
XCTAssertEqual(model.runtime?.platform, "iOS")
|
||||
XCTAssertTrue(model.runtime?.systemVersion.hasPrefix("iOS") ?? false)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user