import SwiftUI /// T-iOS-34 · 设置页 —— 目前只有一件事:**主题**(跟随系统 / 深色 / 浅色)。 /// /// 刻意不做成「一堆开关」:终端字号跟随系统 Dynamic Type(`TerminalTheme` 取 /// `.footnote` 度量),主机/令牌在配对页,快捷键栏在终端工具栏 —— 那些都有更近 /// 的入口,搬到这里只会多一条重复路径。 /// /// 选择即生效:`ThemeStore.select` 落盘 + `@Observable` 触发根视图重算 /// `preferredColorScheme`,不需要「保存」按钮。 struct SettingsScreen: View { /// 主题真值(`RootView` 持有并注入;这里只读写它,不自己存一份)。 let themeStore: ThemeStore /// 当前**有效**外观 —— 根视图的 `preferredColorScheme` 已经把它压进了环境, /// 所以终端预览直接读环境即可,不必再算一遍「跟随系统时到底是哪档」。 @Environment(\.colorScheme) private var effectiveScheme var body: some View { List { themeSection terminalPreviewSection } .navigationTitle(SettingsCopy.title) } // MARK: - 主题选择 private var themeSection: some View { Section { ForEach(AppTheme.allCases, id: \.self) { theme in themeRow(theme) } } header: { Text(SettingsCopy.themeHeader) } footer: { Text(SettingsCopy.themeFooter) } } private func themeRow(_ theme: AppTheme) -> some View { Button { themeStore.select(theme) DS.Haptics.selection() } label: { HStack(spacing: DS.Space.md12) { Image(systemName: theme.symbolName) .foregroundStyle(DS.Palette.accent) .frame(width: DS.Space.xxl24) Text(theme.label) .foregroundStyle(DS.Palette.textPrimary) Spacer(minLength: DS.Space.sm8) if themeStore.theme == theme { Image(systemName: "checkmark") .foregroundStyle(DS.Palette.accent) .fontWeight(.semibold) } } .frame(minHeight: DS.Layout.minHitTarget) } .accessibilityIdentifier("settings.theme.\(theme.rawValue)") // 选中态对 VoiceOver 可见(勾号是视觉信号,不是语义信号)。 .accessibilityAddTraits(themeStore.theme == theme ? [.isSelected] : []) } // MARK: - 终端画布预览 /// 主题的最大可见差异就是终端那一整块画布,所以给一个真实取色的预览条: /// 背景/前景/光标全部来自 `TerminalPalette`(与真终端同一出处)。 private var terminalPreviewSection: some View { Section { let colors = TerminalPalette.colors(for: effectiveScheme) HStack(spacing: DS.Space.xs2) { Text(verbatim: SettingsCopy.terminalSample) .font(DS.Typography.mono(.footnote)) .foregroundStyle(Color(uiColor: colors.foreground)) Rectangle() .fill(Color(uiColor: colors.caret)) .frame(width: DS.Space.sm8, height: DS.Space.lg16) Spacer(minLength: 0) } .padding(DS.Space.md12) .background( Color(uiColor: colors.background), in: RoundedRectangle(cornerRadius: DS.Radius.sm8) ) .accessibilityElement(children: .ignore) .accessibilityLabel(SettingsCopy.terminalPreviewA11y) } header: { Text(SettingsCopy.terminalHeader) } } } /// 设置页用户可见文案。 enum SettingsCopy { static let title = "设置" static let themeHeader = "外观" static let themeFooter = "默认深色 —— 与网页端一致。选「跟随系统」时随 iOS 的浅色/深色切换。" static let terminalHeader = "终端预览" static let terminalSample = "$ claude --resume" static let terminalPreviewA11y = "终端配色预览" } // MARK: - Previews #Preview("SettingsScreen") { NavigationStack { SettingsScreen(themeStore: ThemeStore(defaults: PreviewThemeDefaults())) } } /// 预览用的内存 defaults —— 预览绝不写用户的真实 `UserDefaults`。 private final class PreviewThemeDefaults: ThemeDefaults { private var values: [String: String] = [:] func themeRaw(forKey key: String) -> String? { values[key] } func setThemeRaw(_ value: String, forKey key: String) { values = values.merging([key: value]) { _, new in new } } }