diff --git a/Resources/Skills/supacode-cli.md b/Resources/Skills/supacode-cli.md index 33f419aa1..6dad6c6a3 100644 --- a/Resources/Skills/supacode-cli.md +++ b/Resources/Skills/supacode-cli.md @@ -144,7 +144,7 @@ supacode repo worktree-new [-r ] [--branch ] [--base ] [--upstrea ### Settings ``` -supacode settings [
] # Open settings (general|notifications|worktrees|developer|shortcuts|scripts|updates|github). +supacode settings [
] # Open settings (general|accessibility|notifications|worktrees|developer|shortcuts|scripts|updates|github). supacode settings repo [-r ] # Open repository settings. supacode settings repo scripts [-r ] # Open repository Scripts settings. ``` diff --git a/Resources/Skills/supacode-deeplinks.md b/Resources/Skills/supacode-deeplinks.md index 6a82351fc..a02d9f575 100644 --- a/Resources/Skills/supacode-deeplinks.md +++ b/Resources/Skills/supacode-deeplinks.md @@ -83,7 +83,7 @@ supacode://repo//worktree/new?branch=&base=&upstream=&f ``` supacode://settings # Open settings. -supacode://settings/
# general|notifications|worktrees|developer|shortcuts|scripts|updates|github. +supacode://settings/
# general|accessibility|notifications|worktrees|developer|shortcuts|scripts|updates|github. supacode://settings/repo/ # Open repository settings. supacode://settings/repo//scripts # Open repository Scripts settings. ``` diff --git a/SupacodeSettingsFeature/Reducer/SettingsFeature.swift b/SupacodeSettingsFeature/Reducer/SettingsFeature.swift index 89d42fdd1..362c3e78b 100644 --- a/SupacodeSettingsFeature/Reducer/SettingsFeature.swift +++ b/SupacodeSettingsFeature/Reducer/SettingsFeature.swift @@ -82,6 +82,7 @@ public struct SettingsFeature { public var remoteSessionPersistenceEnabled: Bool public var appVisibility: AppVisibility public var terminalHibernationEnabled: Bool + public var chromeTextSize: ChromeTextSize public var cliInstallState = CLIInstallState.checking /// Installed editors in menu order, resolved once off the picker's body. public var installedOpenActions: [OpenWorktreeAction] @@ -158,6 +159,7 @@ public struct SettingsFeature { remoteSessionPersistenceEnabled = settings.remoteSessionPersistenceEnabled appVisibility = settings.appVisibility terminalHibernationEnabled = settings.terminalHibernationEnabled + chromeTextSize = settings.chromeTextSize defaultWorktreeBaseDirectoryPath = SupacodePaths.normalizedWorktreeBaseDirectoryPath(settings.defaultWorktreeBaseDirectoryPath) ?? "" } @@ -200,7 +202,8 @@ public struct SettingsFeature { terminateSessionsOnQuit: terminateSessionsOnQuit, remoteSessionPersistenceEnabled: remoteSessionPersistenceEnabled, appVisibility: appVisibility, - terminalHibernationEnabled: terminalHibernationEnabled + terminalHibernationEnabled: terminalHibernationEnabled, + chromeTextSize: chromeTextSize ) } } @@ -340,6 +343,7 @@ public struct SettingsFeature { state.remoteSessionPersistenceEnabled = normalizedSettings.remoteSessionPersistenceEnabled state.appVisibility = normalizedSettings.appVisibility state.terminalHibernationEnabled = normalizedSettings.terminalHibernationEnabled + state.chromeTextSize = normalizedSettings.chromeTextSize state.defaultWorktreeBaseDirectoryPath = normalizedSettings.defaultWorktreeBaseDirectoryPath ?? "" state.syncGlobalDefaults(from: normalizedSettings) synchronizeRepositorySelection(for: &state) diff --git a/SupacodeSettingsFeature/Views/AccessibilitySettingsView.swift b/SupacodeSettingsFeature/Views/AccessibilitySettingsView.swift new file mode 100644 index 000000000..047a8ed75 --- /dev/null +++ b/SupacodeSettingsFeature/Views/AccessibilitySettingsView.swift @@ -0,0 +1,30 @@ +import ComposableArchitecture +import SupacodeSettingsShared +import SwiftUI + +/// Accessibility pane. Holds the chrome text size, which is separate from the +/// terminal's own font zoom. +public struct AccessibilitySettingsView: View { + @Bindable var store: StoreOf + + public init(store: StoreOf) { + self.store = store + } + + public var body: some View { + Form { + Section { + Picker(selection: $store.chromeTextSize) { + ForEach(ChromeTextSize.allCases) { size in + Text(size.label).tag(size) + } + } label: { + Text("Text size") + Text("Sizes the sidebar, tab bar, toolbars, and Settings text. The terminal has its own font size.") + } + } + } + .formStyle(.grouped) + .navigationTitle("Accessibility") + } +} diff --git a/SupacodeSettingsFeature/Views/AppVisibilityOptionCardView.swift b/SupacodeSettingsFeature/Views/AppVisibilityOptionCardView.swift index d5a3d9bbc..54446ea55 100644 --- a/SupacodeSettingsFeature/Views/AppVisibilityOptionCardView.swift +++ b/SupacodeSettingsFeature/Views/AppVisibilityOptionCardView.swift @@ -23,7 +23,7 @@ struct AppVisibilityOptionCardView: View { ) } Text(visibility.title) - .font(.callout) + .appFont(.callout) .multilineTextAlignment(.center) .fixedSize(horizontal: false, vertical: true) .foregroundStyle(isSelected ? .primary : .secondary) diff --git a/SupacodeSettingsFeature/Views/AppearanceOptionCardView.swift b/SupacodeSettingsFeature/Views/AppearanceOptionCardView.swift index 071ee6d20..507561110 100644 --- a/SupacodeSettingsFeature/Views/AppearanceOptionCardView.swift +++ b/SupacodeSettingsFeature/Views/AppearanceOptionCardView.swift @@ -22,7 +22,7 @@ struct AppearanceOptionCardView: View { ) } Text(mode.title) - .font(.callout) + .appFont(.callout) .foregroundStyle(isSelected ? .primary : .secondary) } } diff --git a/SupacodeSettingsFeature/Views/AppearanceSettingsView.swift b/SupacodeSettingsFeature/Views/AppearanceSettingsView.swift index 9bc9ad1d0..86552f5c0 100644 --- a/SupacodeSettingsFeature/Views/AppearanceSettingsView.swift +++ b/SupacodeSettingsFeature/Views/AppearanceSettingsView.swift @@ -161,8 +161,7 @@ public struct AppearanceSettingsView: View { private struct BetaBadge: View { var body: some View { Text("Beta") - .font(.caption2) - .fontWeight(.semibold) + .appFont(.caption2, weight: .semibold) .foregroundStyle(.secondary) .padding(.horizontal, 6) .padding(.vertical, 2) diff --git a/SupacodeSettingsFeature/Views/GlobalScriptsSettingsView.swift b/SupacodeSettingsFeature/Views/GlobalScriptsSettingsView.swift index d8c60fd19..086d83c07 100644 --- a/SupacodeSettingsFeature/Views/GlobalScriptsSettingsView.swift +++ b/SupacodeSettingsFeature/Views/GlobalScriptsSettingsView.swift @@ -59,7 +59,7 @@ public struct GlobalScriptsSettingsView: View { } header: { Label { Text("\(script.displayName) Script") - .font(.body) + .appFont(.body) .bold() } icon: { Image(systemName: script.resolvedSystemImage) diff --git a/SupacodeSettingsFeature/Views/RepositoryScriptsSettingsView.swift b/SupacodeSettingsFeature/Views/RepositoryScriptsSettingsView.swift index 43b3c3c1c..98aeb2f60 100644 --- a/SupacodeSettingsFeature/Views/RepositoryScriptsSettingsView.swift +++ b/SupacodeSettingsFeature/Views/RepositoryScriptsSettingsView.swift @@ -66,7 +66,7 @@ public struct RepositoryScriptsSettingsView: View { } header: { Label { Text("\(script.displayName) Script") - .font(.body) + .appFont(.body) .bold() } icon: { Image(systemName: script.resolvedSystemImage).foregroundStyle(script.resolvedTintColor.color) @@ -134,11 +134,11 @@ private struct LifecycleScriptSection: View { Label { VStack(alignment: .leading, spacing: 0) { Text(title) - .font(.body) + .appFont(.body) .bold() .lineLimit(1) Text(subtitle) - .font(.footnote) + .appFont(.footnote) .foregroundStyle(.secondary) .lineLimit(1) } diff --git a/SupacodeSettingsFeature/Views/RepositorySettingsView.swift b/SupacodeSettingsFeature/Views/RepositorySettingsView.swift index 4727b6c41..f77633e25 100644 --- a/SupacodeSettingsFeature/Views/RepositorySettingsView.swift +++ b/SupacodeSettingsFeature/Views/RepositorySettingsView.swift @@ -64,7 +64,7 @@ public struct RepositorySettingsView: View { .disabled(store.isBareRepository) if store.isBareRepository { Text("Copy flags are ignored for bare repositories.") - .font(.footnote) + .appFont(.footnote) .foregroundStyle(.tertiary) } TextField( diff --git a/SupacodeSettingsFeature/Views/SettingsSection.swift b/SupacodeSettingsFeature/Views/SettingsSection.swift index f1019f171..732bd858f 100644 --- a/SupacodeSettingsFeature/Views/SettingsSection.swift +++ b/SupacodeSettingsFeature/Views/SettingsSection.swift @@ -2,6 +2,7 @@ import Foundation public enum SettingsSection: Hashable { case general + case accessibility case notifications case worktree case developer diff --git a/SupacodeSettingsShared/Models/ChromeTextSize.swift b/SupacodeSettingsShared/Models/ChromeTextSize.swift new file mode 100644 index 000000000..59d1b4ce5 --- /dev/null +++ b/SupacodeSettingsShared/Models/ChromeTextSize.swift @@ -0,0 +1,38 @@ +import Foundation + +/// Size of the app chrome's text: the sidebar, tab bar, toolbars, and Settings. +/// A fixed set of sizes rather than a free-form scale, so the layout only has to +/// hold at three known points. +/// +/// The size is applied by resolving each semantic font explicitly (see +/// `View.appFont`), because macOS SwiftUI does not resize text in response to +/// `dynamicTypeSize`. The Ghostty terminal surface is NSView-backed with its own +/// font system and is unaffected. +public nonisolated enum ChromeTextSize: String, CaseIterable, Codable, Identifiable, Sendable { + case standard + case large + case extraLarge + + public static let `default` = ChromeTextSize.standard + + public var id: String { rawValue } + + public var label: String { + switch self { + case .standard: "Default" + case .large: "Large" + case .extraLarge: "Extra Large" + } + } + + /// Multiplier applied to each semantic font's system point size. macOS has no + /// Dynamic Type step table to inherit, so the two steps are chosen to land + /// near where iOS's `.xLarge` and `.xxLarge` sit. + public var scale: Double { + switch self { + case .standard: 1.0 + case .large: 1.15 + case .extraLarge: 1.3 + } + } +} diff --git a/SupacodeSettingsShared/Models/GlobalSettings.swift b/SupacodeSettingsShared/Models/GlobalSettings.swift index bb665163a..10ff26135 100644 --- a/SupacodeSettingsShared/Models/GlobalSettings.swift +++ b/SupacodeSettingsShared/Models/GlobalSettings.swift @@ -104,6 +104,9 @@ public nonisolated struct GlobalSettings: Codable, Equatable, Sendable { /// Beta: hidden terminal tabs release their renderer after a few minutes of /// inactivity and reconnect when viewed. On by default. public var terminalHibernationEnabled: Bool + /// Accessibility size for the app chrome's text. Drives the scale published at + /// each window root. Defaults to the unmodified system size. + public var chromeTextSize: ChromeTextSize public static let `default` = GlobalSettings( appearanceMode: .dark, @@ -139,7 +142,8 @@ public nonisolated struct GlobalSettings: Codable, Equatable, Sendable { confirmCloseSurface: true, terminateSessionsOnQuit: false, remoteSessionPersistenceEnabled: true, - appVisibility: .dockAndMenuBar + appVisibility: .dockAndMenuBar, + chromeTextSize: .default ) public init( @@ -177,7 +181,8 @@ public nonisolated struct GlobalSettings: Codable, Equatable, Sendable { terminateSessionsOnQuit: Bool = false, remoteSessionPersistenceEnabled: Bool = true, appVisibility: AppVisibility = .dockAndMenuBar, - terminalHibernationEnabled: Bool = true + terminalHibernationEnabled: Bool = true, + chromeTextSize: ChromeTextSize = .default ) { self.appearanceMode = appearanceMode self.defaultEditorID = defaultEditorID @@ -214,6 +219,7 @@ public nonisolated struct GlobalSettings: Codable, Equatable, Sendable { self.remoteSessionPersistenceEnabled = remoteSessionPersistenceEnabled self.appVisibility = appVisibility self.terminalHibernationEnabled = terminalHibernationEnabled + self.chromeTextSize = chromeTextSize } /// Keys for reading renamed settings fields that no longer @@ -388,5 +394,12 @@ public nonisolated struct GlobalSettings: Codable, Equatable, Sendable { terminalHibernationEnabled = try container.decodeIfPresent(Bool.self, forKey: .terminalHibernationEnabled) ?? Self.default.terminalHibernationEnabled + // Old settings files predate this key; they migrate to the system size. An + // unrecognized value falls back the same way rather than throwing, which + // would reset the whole file to defaults. + chromeTextSize = + ((try? container.decodeIfPresent(String.self, forKey: .chromeTextSize)) ?? nil) + .flatMap(ChromeTextSize.init(rawValue:)) + ?? Self.default.chromeTextSize } } diff --git a/SupacodeSettingsShared/Support/UITextScale.swift b/SupacodeSettingsShared/Support/UITextScale.swift new file mode 100644 index 000000000..5e5b4e337 --- /dev/null +++ b/SupacodeSettingsShared/Support/UITextScale.swift @@ -0,0 +1,137 @@ +import AppKit +import SwiftUI + +/// Environment carrier for the app-wide UI text scale (1.0 = system default). +/// Set at each window root from `GlobalSettings.chromeTextSize`; read by +/// `View.appFont(_:)`. Using an explicit scale (rather than `dynamicTypeSize`) +/// is required because macOS SwiftUI does not resize text for Dynamic Type. +/// +/// Lives in the shared module so every UI module (app + settings feature) routes +/// its chrome text through the same helper. +private struct UITextScaleKey: EnvironmentKey { + static let defaultValue: Double = 1.0 +} + +extension EnvironmentValues { + public var uiTextScale: Double { + get { self[UITextScaleKey.self] } + set { self[UITextScaleKey.self] = newValue } + } +} + +/// Base point sizes and default weights for the semantic text styles, read from +/// the live system fonts so scaled sizes track the platform instead of a +/// hardcoded table. Read once: macOS resolves the text-style table at launch and +/// does not change it while the app runs, and the scaled path would otherwise +/// hit AppKit on every body evaluation of every scaled view. +private enum AppFontMetrics { + private static let pointSizes: [Font.TextStyle: CGFloat] = { + let styles: [Font.TextStyle] = [ + .largeTitle, .title, .title2, .title3, .headline, .subheadline, + .body, .callout, .footnote, .caption, .caption2, + ] + return Dictionary( + uniqueKeysWithValues: styles.map { + ($0, NSFont.preferredFont(forTextStyle: nsTextStyle(for: $0)).pointSize) + } + ) + }() + + static func pointSize(for style: Font.TextStyle) -> CGFloat { + pointSizes[style] ?? NSFont.preferredFont(forTextStyle: .body).pointSize + } + + /// The semantic style's default weight, preserved when the caller doesn't + /// override it. Only `.headline` deviates from regular on macOS. + static func defaultWeight(for style: Font.TextStyle) -> Font.Weight { + style == .headline ? .semibold : .regular + } + + private static func nsTextStyle(for style: Font.TextStyle) -> NSFont.TextStyle { + switch style { + case .largeTitle: .largeTitle + case .title: .title1 + case .title2: .title2 + case .title3: .title3 + case .headline: .headline + case .subheadline: .subheadline + case .body: .body + case .callout: .callout + case .footnote: .footnote + case .caption: .caption1 + case .caption2: .caption2 + @unknown default: .body + } + } +} + +private struct AppFontModifier: ViewModifier { + @Environment(\.uiTextScale) private var scale + let style: Font.TextStyle + let weight: Font.Weight? + let monospaced: Bool + + func body(content: Content) -> some View { + content.font(resolvedFont) + } + + private var resolvedFont: Font { + // At 1.0× use the exact semantic font, so Default leaves text untouched. + if scale == 1.0 { + var font = Font.system(style) + if let weight { font = font.weight(weight) } + if monospaced { font = font.monospaced() } + return font + } + // Rounded: a fractional point size gives a fractional line height, which + // drifts baselines across the sidebar's dense rows. + let scaledSize = (AppFontMetrics.pointSize(for: style) * scale).rounded() + let font = Font.system(size: scaledSize, design: monospaced ? .monospaced : .default) + return font.weight(weight ?? AppFontMetrics.defaultWeight(for: style)) + } +} + +/// Scales text that has no font of its own. `List` styles its own section +/// headers and SwiftUI does not expose the font it resolved, so there is nothing +/// to scale in place — `base` stands in for it. At 1.0× the view is left alone, +/// which keeps the platform header styling as it ships. +private struct AppFontInheritedModifier: ViewModifier { + @Environment(\.uiTextScale) private var scale + let base: Font.TextStyle + let weight: Font.Weight? + + func body(content: Content) -> some View { + if scale == 1.0 { + content + } else { + content.font( + Font.system(size: (AppFontMetrics.pointSize(for: base) * scale).rounded()) + .weight(weight ?? AppFontMetrics.defaultWeight(for: base)) + ) + } + } +} + +extension View { + /// Publishes the chrome text size to descendants. Applied at each window root; + /// text opts in with `.appFont(_:)`. + public func appChromeTextSize(_ size: ChromeTextSize) -> some View { + environment(\.uiTextScale, size.scale) + } + + /// Applies a semantic text style that honors the chrome text size. Use this in + /// place of `.font(.body)` etc. for chrome text that should follow the user's + /// chosen size. `weight` overrides the style's default weight; `monospaced` + /// picks the monospaced design. + public func appFont(_ style: Font.TextStyle, weight: Font.Weight? = nil, monospaced: Bool = false) -> some View { + modifier(AppFontModifier(style: style, weight: weight, monospaced: monospaced)) + } + + /// Scales text whose font comes from an enclosing container rather than from + /// this view — the sidebar's `Section` headers. At 1.0× the container's + /// styling is left in place; `base` only sets the point size the text grows + /// from above that. + public func appFontInheriting(_ base: Font.TextStyle, weight: Font.Weight? = nil) -> some View { + modifier(AppFontInheritedModifier(base: base, weight: weight)) + } +} diff --git a/supacode-cli/Commands/SettingsCommand.swift b/supacode-cli/Commands/SettingsCommand.swift index 138563074..677bd182c 100644 --- a/supacode-cli/Commands/SettingsCommand.swift +++ b/supacode-cli/Commands/SettingsCommand.swift @@ -6,6 +6,7 @@ struct SettingsCommand: ParsableCommand { abstract: "Open Supacode settings.", subcommands: [ General.self, + Accessibility.self, Notifications.self, Worktrees.self, Developer.self, @@ -31,6 +32,7 @@ extension SettingsCommand { /// Raw values must match `Deeplink.DeeplinkSettingsSection` on the app side. fileprivate enum Section: String { case general + case accessibility case notifications case worktrees case developer @@ -46,6 +48,12 @@ extension SettingsCommand { func run() throws { try dispatchSettings(.general, timeoutSeconds: timeoutOption.timeout) } } + struct Accessibility: ParsableCommand { + static let configuration = CommandConfiguration(abstract: "Open Accessibility settings.") + @OptionGroup var timeoutOption: TimeoutOption + func run() throws { try dispatchSettings(.accessibility, timeoutSeconds: timeoutOption.timeout) } + } + struct Notifications: ParsableCommand { static let configuration = CommandConfiguration(abstract: "Open Notifications settings.") @OptionGroup var timeoutOption: TimeoutOption diff --git a/supacode/App/CLIReferenceView.swift b/supacode/App/CLIReferenceView.swift index abc52838c..021b1296b 100644 --- a/supacode/App/CLIReferenceView.swift +++ b/supacode/App/CLIReferenceView.swift @@ -1,3 +1,4 @@ +import SupacodeSettingsShared import SwiftUI struct CLIReferenceView: View { @@ -19,7 +20,7 @@ struct CLIReferenceView: View { .foregroundStyle(.secondary) // swiftlint:enable line_length } header: { - Text("CLI Reference").font(.title.bold()) + Text("CLI Reference").appFont(.title, weight: .bold) Text("Control Supacode from the terminal.") } @@ -185,7 +186,7 @@ private struct CLISection: View { ForEach(rows) { row in GridRow { Text(row.command) - .font(.body.monospaced()) + .appFont(.body, monospaced: true) .gridColumnAlignment(.leading) Text(row.description) .foregroundStyle(.secondary) diff --git a/supacode/App/DeeplinkReferenceView.swift b/supacode/App/DeeplinkReferenceView.swift index 74a81b33b..ce7fe19d4 100644 --- a/supacode/App/DeeplinkReferenceView.swift +++ b/supacode/App/DeeplinkReferenceView.swift @@ -1,4 +1,5 @@ import ComposableArchitecture +import SupacodeSettingsShared import SwiftUI struct DeeplinkReferenceView: View { @@ -26,7 +27,7 @@ struct DeeplinkReferenceView: View { ) .foregroundStyle(.secondary) } header: { - Text("Deeplink Reference").font(.title.bold()) + Text("Deeplink Reference").appFont(.title, weight: .bold) Text("Use the \(code("supacode://")) URL scheme to control Supacode from the terminal, scripts, or other apps.") } @@ -129,7 +130,7 @@ struct DeeplinkReferenceView: View { .init( url: "supacode://settings/
", description: "Open a specific section.", - params: "general|notifications|worktrees|developer|shortcuts|scripts|updates|github" + params: "general|accessibility|notifications|worktrees|developer|shortcuts|scripts|updates|github" ), .init(url: "supacode://settings/repo/", description: "Open repository settings."), .init( @@ -163,7 +164,7 @@ private struct DeeplinkSection: View { ForEach(rows) { row in GridRow { Text(row.url) - .font(.body.monospaced()) + .appFont(.body, monospaced: true) .gridColumnAlignment(.leading) row.descriptionText .foregroundStyle(.secondary) diff --git a/supacode/App/supacodeApp.swift b/supacode/App/supacodeApp.swift index 10e104011..005e4e7bb 100644 --- a/supacode/App/supacodeApp.swift +++ b/supacode/App/supacodeApp.swift @@ -539,6 +539,7 @@ struct SupacodeApp: App { .environment(ghosttyShortcuts) .environment(commandKeyObserver) .environment(openActionIcons) + .appChromeTextSize(store.settings.chromeTextSize) } .openSettingsOnSelection(store: store) .openDeeplinkReferenceOnRequest(store: store) @@ -601,6 +602,7 @@ struct SupacodeApp: App { SettingsView(store: store) .environment(ghosttyShortcuts) .environment(commandKeyObserver) + .appChromeTextSize(store.settings.chromeTextSize) .toolbarBackground(.hidden, for: .windowToolbar) .toolbarColorScheme(store.settings.appearanceMode.colorScheme, for: .windowToolbar) .movesSettingsWindowToActiveSpace() @@ -611,6 +613,7 @@ struct SupacodeApp: App { .restorationBehavior(.disabled) Window("Deeplink Reference", id: WindowID.deeplinkReference) { DeeplinkReferenceView() + .appChromeTextSize(store.settings.chromeTextSize) } .handlesExternalEvents(matching: []) .windowToolbarStyle(.unified) @@ -618,6 +621,7 @@ struct SupacodeApp: App { .restorationBehavior(.disabled) Window("CLI Reference", id: WindowID.cliReference) { CLIReferenceView() + .appChromeTextSize(store.settings.chromeTextSize) } .handlesExternalEvents(matching: []) .windowToolbarStyle(.unified) @@ -625,6 +629,7 @@ struct SupacodeApp: App { .restorationBehavior(.disabled) MenuBarExtra(isInserted: menuBarInserted) { MenuBarNotificationsMenu(store: store) + .appChromeTextSize(store.settings.chromeTextSize) } label: { MenuBarNotificationsLabel(unreadCount: store.notificationIndicatorCount) } diff --git a/supacode/Domain/Deeplink.swift b/supacode/Domain/Deeplink.swift index 8ac97d6a8..0f45762c2 100644 --- a/supacode/Domain/Deeplink.swift +++ b/supacode/Domain/Deeplink.swift @@ -58,6 +58,7 @@ enum Deeplink: Equatable, Sendable { /// Settings sections reachable via deeplink. enum DeeplinkSettingsSection: String, Equatable, Sendable { case general + case accessibility case notifications case worktrees case developer diff --git a/supacode/Features/AgentPresence/Views/CodingAgentsSidebarCardView.swift b/supacode/Features/AgentPresence/Views/CodingAgentsSidebarCardView.swift index 97819eacd..64dc00036 100644 --- a/supacode/Features/AgentPresence/Views/CodingAgentsSidebarCardView.swift +++ b/supacode/Features/AgentPresence/Views/CodingAgentsSidebarCardView.swift @@ -97,7 +97,7 @@ private struct CodingAgentsCardBody: View { openWindow(id: WindowID.settings) } .buttonStyle(.link) - .font(.caption) + .appFont(.caption) .padding(.top, 2) } } header: { diff --git a/supacode/Features/App/Reducer/AppFeature.swift b/supacode/Features/App/Reducer/AppFeature.swift index 3e9fc609d..7d01c3082 100644 --- a/supacode/Features/App/Reducer/AppFeature.swift +++ b/supacode/Features/App/Reducer/AppFeature.swift @@ -3356,6 +3356,7 @@ struct AppFeature { let settingsSection: SettingsSection = switch section { case .general: .general + case .accessibility: .accessibility case .notifications: .notifications case .worktrees: .worktree case .developer: .developer diff --git a/supacode/Features/App/Views/MenuBarNotificationsMenu.swift b/supacode/Features/App/Views/MenuBarNotificationsMenu.swift index 7fd6c049e..719eca716 100644 --- a/supacode/Features/App/Views/MenuBarNotificationsMenu.swift +++ b/supacode/Features/App/Views/MenuBarNotificationsMenu.swift @@ -209,7 +209,7 @@ private struct MenuBarSectionHeader: View { var body: some View { HStack(spacing: 4) { Text(title) - .font(.caption) + .appFont(.caption) .fontWeight(.semibold) .foregroundStyle(.secondary) if let dotColor { diff --git a/supacode/Features/Repositories/Views/ArchivedWorktreeRowView.swift b/supacode/Features/Repositories/Views/ArchivedWorktreeRowView.swift index 6a5515c14..2b64510c6 100644 --- a/supacode/Features/Repositories/Views/ArchivedWorktreeRowView.swift +++ b/supacode/Features/Repositories/Views/ArchivedWorktreeRowView.swift @@ -28,7 +28,7 @@ struct ArchivedWorktreeRowView: View { VStack(alignment: .leading, spacing: 2) { HStack(alignment: .firstTextBaseline, spacing: 8) { Image(systemName: "archivebox") - .font(.caption) + .appFont(.caption) .foregroundStyle(.secondary) .accessibilityHidden(true) .frame(width: 16, height: 16) @@ -36,7 +36,7 @@ struct ArchivedWorktreeRowView: View { bodyFontAscender } let titleText = Text(displayName) - .font(.body) + .appFont(.body) .lineLimit(1) if let customTint, backgroundProminence != .increased { titleText.foregroundStyle(customTint.color) @@ -71,7 +71,7 @@ struct ArchivedWorktreeRowView: View { Spacer(minLength: 0) WorktreePullRequestAccessoryView(display: display) } - .font(.caption) + .appFont(.caption) .lineLimit(1) .frame(minHeight: 14) .padding(.leading, 24) diff --git a/supacode/Features/Repositories/Views/ArchivedWorktreesDetailView.swift b/supacode/Features/Repositories/Views/ArchivedWorktreesDetailView.swift index e497a75ec..557873c95 100644 --- a/supacode/Features/Repositories/Views/ArchivedWorktreesDetailView.swift +++ b/supacode/Features/Repositories/Views/ArchivedWorktreesDetailView.swift @@ -137,16 +137,16 @@ private struct ArchivedWorktreeSectionHeader: View { } label: { HStack(spacing: 6) { Image(systemName: "chevron.right") - .font(.caption2) + .appFont(.caption2) .rotationEffect(.degrees(isCollapsed ? 0 : 90)) .foregroundStyle(.secondary) .accessibilityHidden(true) Text(name) - .font(.headline) + .appFont(.headline) .foregroundStyle(.primary) .lineLimit(1) Text("(\(worktreeCount))") - .font(.headline) + .appFont(.headline) .foregroundStyle(.secondary) Spacer() } diff --git a/supacode/Features/Repositories/Views/CloneRepositoryFormView.swift b/supacode/Features/Repositories/Views/CloneRepositoryFormView.swift index 4d8275551..74d0cbe54 100644 --- a/supacode/Features/Repositories/Views/CloneRepositoryFormView.swift +++ b/supacode/Features/Repositories/Views/CloneRepositoryFormView.swift @@ -67,7 +67,7 @@ struct CloneRepositoryFormView: View { ProgressView().controlSize(.small) if let progress = store.progressLine, !progress.isEmpty { Text(store.compactProgressLine ?? progress) - .font(.callout) + .appFont(.callout) .foregroundStyle(.secondary) .lineLimit(1) .truncationMode(.middle) diff --git a/supacode/Features/Repositories/Views/EmptyStateView.swift b/supacode/Features/Repositories/Views/EmptyStateView.swift index d67108d25..4a6865a69 100644 --- a/supacode/Features/Repositories/Views/EmptyStateView.swift +++ b/supacode/Features/Repositories/Views/EmptyStateView.swift @@ -12,18 +12,18 @@ struct EmptyStateView: View { VStack(spacing: 12) { Image(systemName: "tray") - .font(.title) + .appFont(.title) .imageScale(.large) .accessibilityHidden(true) .foregroundStyle(.secondary) VStack(spacing: 4) { Text("Open a repository or folder") - .font(.title3) + .appFont(.title3) Text( "Press \(openRepo?.display ?? AppShortcuts.openRepository.display) " + "or click Open Repository or Folder to choose one." ) - .font(.subheadline) + .appFont(.subheadline) .foregroundStyle(.secondary) } Button("Open Repository or Folder...") { diff --git a/supacode/Features/Repositories/Views/GitEnvironmentErrorCardView.swift b/supacode/Features/Repositories/Views/GitEnvironmentErrorCardView.swift index 2737cd569..96a96f27b 100644 --- a/supacode/Features/Repositories/Views/GitEnvironmentErrorCardView.swift +++ b/supacode/Features/Repositories/Views/GitEnvironmentErrorCardView.swift @@ -1,4 +1,5 @@ import AppKit +import SupacodeSettingsShared import SwiftUI /// Non-dismissible sidebar banner shown while the `git` binary is blocked at the @@ -18,7 +19,7 @@ struct GitEnvironmentErrorCardView: View { private struct GitEnvironmentErrorCardIcon: View { var body: some View { Image(systemName: "exclamationmark.triangle.fill") - .font(.title2) + .appFont(.title2) .foregroundStyle(.orange) .accessibilityHidden(true) } @@ -30,10 +31,10 @@ private struct GitEnvironmentErrorCardContent: View { var body: some View { VStack(alignment: .leading, spacing: 8) { Text(error.title) - .font(.subheadline) + .appFont(.subheadline) .fontWeight(.semibold) Text(error.message) - .font(.caption) + .appFont(.caption) .foregroundStyle(.secondary) GitEnvironmentRemedyRow(command: error.remedyCommand) } @@ -46,7 +47,7 @@ private struct GitEnvironmentRemedyRow: View { var body: some View { HStack(spacing: 6) { Text(command) - .font(.caption.monospaced()) + .appFont(.caption, monospaced: true) .textSelection(.enabled) .lineLimit(1) // Truncate the tail so the leading verb (sudo / xcode-select) stays @@ -71,7 +72,7 @@ private struct CopyCommandButton: View { pasteboard.setString(command, forType: .string) } label: { Image(systemName: "doc.on.doc") - .font(.caption) + .appFont(.caption) .contentShape(.rect) } .buttonStyle(.plain) diff --git a/supacode/Features/Repositories/Views/HighlightRelevantOnboardingCardView.swift b/supacode/Features/Repositories/Views/HighlightRelevantOnboardingCardView.swift index 262e658fe..7102d25ab 100644 --- a/supacode/Features/Repositories/Views/HighlightRelevantOnboardingCardView.swift +++ b/supacode/Features/Repositories/Views/HighlightRelevantOnboardingCardView.swift @@ -1,4 +1,5 @@ import Sharing +import SupacodeSettingsShared import SwiftUI /// Bottom-of-sidebar onboarding card surfacing the new "Highlight Relevant @@ -54,14 +55,14 @@ private struct HighlightRelevantOnboardingCardBody: View { VStack(alignment: .leading, spacing: 4) { SidebarCardLabel(title: "Pinned and Active at a glance", description: description) Text("Toggle in View → Group Relevant Sidebar Rows") - .font(.caption2) + .appFont(.caption2) .foregroundStyle(.tertiary) .padding(.top, 2) } }, header: { Image(systemName: "sparkles") - .font(.title2) + .appFont(.title2) .foregroundStyle(.orange) .accessibilityHidden(true) } diff --git a/supacode/Features/Repositories/Views/MenuBarOnboardingCardView.swift b/supacode/Features/Repositories/Views/MenuBarOnboardingCardView.swift index 80c19ed9e..64b4c3dcc 100644 --- a/supacode/Features/Repositories/Views/MenuBarOnboardingCardView.swift +++ b/supacode/Features/Repositories/Views/MenuBarOnboardingCardView.swift @@ -1,4 +1,5 @@ import Sharing +import SupacodeSettingsShared import SwiftUI /// Bottom-of-sidebar onboarding card announcing that Supacode now lives in the @@ -45,14 +46,14 @@ private struct MenuBarOnboardingCardBody: View { VStack(alignment: .leading, spacing: 4) { SidebarCardLabel(title: "Supacode in the menu bar", description: description) Text("Turn off in Settings → General") - .font(.caption2) + .appFont(.caption2) .foregroundStyle(.tertiary) .padding(.top, 2) } }, header: { Image(systemName: "menubar.rectangle") - .font(.title2) + .appFont(.title2) .foregroundStyle(.green) .accessibilityHidden(true) } diff --git a/supacode/Features/Repositories/Views/NestedWorktreesOnboardingCardView.swift b/supacode/Features/Repositories/Views/NestedWorktreesOnboardingCardView.swift index 422e518d9..81ea69b8e 100644 --- a/supacode/Features/Repositories/Views/NestedWorktreesOnboardingCardView.swift +++ b/supacode/Features/Repositories/Views/NestedWorktreesOnboardingCardView.swift @@ -1,4 +1,5 @@ import Sharing +import SupacodeSettingsShared import SwiftUI /// Pinned bottom-of-sidebar onboarding card surfacing the new branch-nesting @@ -48,14 +49,14 @@ private struct NestedWorktreesOnboardingCardBody: View { VStack(alignment: .leading, spacing: 4) { SidebarCardLabel(title: "Worktrees nested by branch", description: description) Text("Toggle in View → Nest Worktrees by Branch") - .font(.caption2) + .appFont(.caption2) .foregroundStyle(.tertiary) .padding(.top, 2) } }, header: { Image(systemName: "list.bullet.indent") - .font(.title2) + .appFont(.title2) .foregroundStyle(.blue) .accessibilityHidden(true) } diff --git a/supacode/Features/Repositories/Views/NotificationPopoverView.swift b/supacode/Features/Repositories/Views/NotificationPopoverView.swift index 6a6e789c7..e8b5085f7 100644 --- a/supacode/Features/Repositories/Views/NotificationPopoverView.swift +++ b/supacode/Features/Repositories/Views/NotificationPopoverView.swift @@ -1,3 +1,4 @@ +import SupacodeSettingsShared import SwiftUI struct NotificationPopoverView: View { @@ -10,9 +11,9 @@ struct NotificationPopoverView: View { ScrollView { VStack(alignment: .leading) { Text("Notifications") - .font(.headline) + .appFont(.headline) Text("\(count) \(countLabel)") - .font(.subheadline) + .appFont(.subheadline) .foregroundStyle(.secondary) Divider() ForEach(notifications) { notification in @@ -30,7 +31,7 @@ struct NotificationPopoverView: View { .frame(maxWidth: .infinity, alignment: .leading) } .buttonStyle(.plain) - .font(.caption) + .appFont(.caption) .help(notification.content.isEmpty ? "Focus pane" : notification.content) } } diff --git a/supacode/Features/Repositories/Views/PullRequestBadgeView.swift b/supacode/Features/Repositories/Views/PullRequestBadgeView.swift index db1821ee6..f1ac8e46a 100644 --- a/supacode/Features/Repositories/Views/PullRequestBadgeView.swift +++ b/supacode/Features/Repositories/Views/PullRequestBadgeView.swift @@ -1,3 +1,4 @@ +import SupacodeSettingsShared import SwiftUI enum PullRequestBadgeStyle { @@ -38,7 +39,7 @@ struct PullRequestBadgeView: View { var body: some View { Text(text) - .font(.caption2) + .appFont(.caption2) .foregroundStyle(color) .padding(.horizontal, 6) .padding(.vertical, 2) diff --git a/supacode/Features/Repositories/Views/PullRequestChecksPopoverView.swift b/supacode/Features/Repositories/Views/PullRequestChecksPopoverView.swift index f05e75dc7..1561b3c0e 100644 --- a/supacode/Features/Repositories/Views/PullRequestChecksPopoverView.swift +++ b/supacode/Features/Repositories/Views/PullRequestChecksPopoverView.swift @@ -67,14 +67,14 @@ struct PullRequestChecksPopoverView: View { .focusable(false) .help("Open pull request on GitHub (\(effectiveOpenPR?.display ?? "none"))") .appKeyboardShortcut(effectiveOpenPR) - .font(.headline) + .appFont(.headline) } else { titleLine .lineLimit(1) - .font(.headline) + .appFont(.headline) } summaryLine - .font(.subheadline) + .appFont(.subheadline) .lineLimit(1) HStack { additionsText @@ -88,7 +88,7 @@ struct PullRequestChecksPopoverView: View { .foregroundStyle(.red) } } - .font(.subheadline) + .appFont(.subheadline) if let mergeQueueStatus = PullRequestMergeQueueStatus(pullRequest: pullRequest) { PullRequestMergeQueueRow(status: mergeQueueStatus) @@ -100,7 +100,7 @@ struct PullRequestChecksPopoverView: View { Text(breakdown.summaryText) .foregroundStyle(.secondary) } - .font(.caption) + .appFont(.caption) } if !sortedChecks.isEmpty { @@ -131,7 +131,7 @@ struct PullRequestChecksPopoverView: View { Text(style.label) .foregroundStyle(.secondary) } - .font(.caption) + .appFont(.caption) } } } @@ -157,10 +157,10 @@ struct PullRequestChecksPopoverView: View { Text(status.summary) .foregroundStyle(.brown) } - .font(.subheadline) + .appFont(.subheadline) if let detail = status.detail { Text(detail) - .font(.caption) + .appFont(.caption) .foregroundStyle(.secondary) } } diff --git a/supacode/Features/Repositories/Views/RemoteRepositoriesBetaCardView.swift b/supacode/Features/Repositories/Views/RemoteRepositoriesBetaCardView.swift index 97d9590e5..311174274 100644 --- a/supacode/Features/Repositories/Views/RemoteRepositoriesBetaCardView.swift +++ b/supacode/Features/Repositories/Views/RemoteRepositoriesBetaCardView.swift @@ -1,4 +1,5 @@ import Sharing +import SupacodeSettingsShared import SwiftUI /// Bottom-of-sidebar card announcing the remote SSH repositories feature, marked @@ -41,18 +42,18 @@ private struct RemoteRepositoriesBetaCardBody: View { VStack(alignment: .leading, spacing: 4) { HStack(spacing: 6) { Text("Remote repositories") - .font(.subheadline) + .appFont(.subheadline) .fontWeight(.semibold) BetaBadge() } Text(description) - .font(.caption) + .appFont(.caption) .foregroundStyle(.secondary) } }, header: { Image(systemName: "wifi") - .font(.title2) + .appFont(.title2) .foregroundStyle(.teal) .accessibilityHidden(true) } @@ -71,7 +72,7 @@ private struct RemoteRepositoriesBetaCardBody: View { private struct BetaBadge: View { var body: some View { Text("Beta") - .font(.caption2) + .appFont(.caption2) .fontWeight(.semibold) .foregroundStyle(.teal) .padding(.horizontal, 6) diff --git a/supacode/Features/Repositories/Views/RepoSectionHeaderView.swift b/supacode/Features/Repositories/Views/RepoSectionHeaderView.swift index 852cc01d1..b7532adb2 100644 --- a/supacode/Features/Repositories/Views/RepoSectionHeaderView.swift +++ b/supacode/Features/Repositories/Views/RepoSectionHeaderView.swift @@ -28,6 +28,10 @@ struct RepoSectionHeaderView: View { .accessibilityLabel("Remote host \(hostInfo)") } } + // The repository name is the sidebar's primary label, so it has to follow + // the text scale like the rows beneath it. As a `Section` header it draws + // with the list's font rather than one of its own. + .appFontInheriting(.subheadline, weight: .semibold) if isRemoving { ProgressView() .controlSize(.small) diff --git a/supacode/Features/Repositories/Views/SidebarHighlightSectionsView.swift b/supacode/Features/Repositories/Views/SidebarHighlightSectionsView.swift index 35fbb2c08..a4e20bbed 100644 --- a/supacode/Features/Repositories/Views/SidebarHighlightSectionsView.swift +++ b/supacode/Features/Repositories/Views/SidebarHighlightSectionsView.swift @@ -32,6 +32,7 @@ struct SidebarHighlightSection: View { Text(kind.title) SidebarHighlightHeaderDot(color: kind.indicatorColor) } + .appFontInheriting(.subheadline, weight: .semibold) } } } diff --git a/supacode/Features/Repositories/Views/SidebarItemView.swift b/supacode/Features/Repositories/Views/SidebarItemView.swift index e0378d1a4..42c1825ac 100644 --- a/supacode/Features/Repositories/Views/SidebarItemView.swift +++ b/supacode/Features/Repositories/Views/SidebarItemView.swift @@ -292,7 +292,7 @@ private struct TitleView: View, Equatable { let accentStyle = accent.shapeStyle(emphasized: isEmphasized) VStack(alignment: .leading, spacing: 0) { let titleText = Text(name) - .font(.body) + .appFont(.body) .lineLimit(1) if let customTint, !isEmphasized { titleText.foregroundStyle(customTint.color).shimmer(isActive: isBusy) @@ -304,7 +304,7 @@ private struct TitleView: View, Equatable { EmptyView() case .plain(let text): Text(text) - .font(.footnote) + .appFont(.footnote) .foregroundStyle(accentStyle) .lineLimit(1) case .highlight(let repo, let repoColor, let trail, let hostInfo): @@ -336,7 +336,7 @@ private struct TitleView: View, Equatable { .lineLimit(1) } } - .font(.footnote) + .appFont(.footnote) .accessibilityElement(children: .combine) .accessibilityLabel(trail.map { "\(repo), \($0)" } ?? repo) } @@ -529,7 +529,7 @@ private struct TrailingView: View { if store.kind == .folder, let host = store.host { Image(systemName: "wifi") .imageScale(.small) - .font(.subheadline) + .appFont(.subheadline) .foregroundStyle(.secondary) .help(host.displayAuthority) .accessibilityLabel("Remote host \(host.displayAuthority)") @@ -564,7 +564,7 @@ private struct TrailingView: View { .allowsHitTesting(!hasHint) Text(shortcutHint ?? "") - .font(.caption) + .appFont(.caption) .foregroundStyle(.secondary) .opacity(hasHint ? 1 : 0) } @@ -580,7 +580,7 @@ private struct SidebarDormantIndicator: View, Equatable { // descending tail skews the optical center; compensate to match the wifi glyph. Image(systemName: "zzz") .imageScale(.small) - .font(.subheadline.weight(.semibold)) + .appFont(.subheadline, weight: .semibold) .offset(y: 0.5) .foregroundStyle(.secondary) .help("Hibernated to save resources. Select to reconnect.") @@ -593,7 +593,7 @@ private struct PullRequestBadgeContent: View, Equatable { var body: some View { Text(text) - .font(.caption) + .appFont(.caption) .foregroundStyle(.secondary) .transition(.blurReplace) } @@ -625,7 +625,7 @@ private struct DiffStatsContent: View, Equatable { Text("-\(removedLines)") .foregroundStyle(isEmphasized ? AnyShapeStyle(.secondary) : AnyShapeStyle(.red)) } - .font(.caption) + .appFont(.caption) .monospacedDigit() .transition(.blurReplace) } diff --git a/supacode/Features/Repositories/Views/SidebarItemsView.swift b/supacode/Features/Repositories/Views/SidebarItemsView.swift index 1f338e07f..5cd91a0fa 100644 --- a/supacode/Features/Repositories/Views/SidebarItemsView.swift +++ b/supacode/Features/Repositories/Views/SidebarItemsView.swift @@ -276,7 +276,7 @@ private struct SidebarPathGroupHeaderRow: View { } label: { HStack(spacing: 6) { Image(systemName: "chevron.right") - .font(.caption.weight(.semibold)) + .appFont(.caption, weight: .semibold) .foregroundStyle(.secondary) .rotationEffect(.degrees(isCollapsed ? 0 : 90)) .animation(.easeInOut(duration: 0.15), value: isCollapsed) @@ -286,7 +286,7 @@ private struct SidebarPathGroupHeaderRow: View { ) .accessibilityHidden(true) Text(label) - .font(.body) + .appFont(.body) .lineLimit(1) .foregroundStyle(.primary) Spacer(minLength: 0) diff --git a/supacode/Features/Repositories/Views/SidebarListView.swift b/supacode/Features/Repositories/Views/SidebarListView.swift index 055d5c16c..b7c705022 100644 --- a/supacode/Features/Repositories/Views/SidebarListView.swift +++ b/supacode/Features/Repositories/Views/SidebarListView.swift @@ -339,7 +339,7 @@ private struct SidebarHoistSummaryRow: View { } Spacer(minLength: 0) } - .font(.caption) + .appFont(.caption) .foregroundStyle(.secondary) .lineLimit(1) .contentShape(.interaction, .rect) @@ -544,12 +544,12 @@ private struct SidebarPlaceholderView: View { Label { VStack(alignment: .leading, spacing: 2) { Text("placeholder-branch") - .font(.body) + .appFont(.body) .lineLimit(1) .redacted(reason: .placeholder) .shimmer(isActive: true) Text("placeholder") - .font(.footnote) + .appFont(.footnote) .lineLimit(1) .redacted(reason: .placeholder) .shimmer(isActive: true) diff --git a/supacode/Features/Repositories/Views/TerminalPersistenceOnboardingCardView.swift b/supacode/Features/Repositories/Views/TerminalPersistenceOnboardingCardView.swift index 3cf32ba2e..f4da04202 100644 --- a/supacode/Features/Repositories/Views/TerminalPersistenceOnboardingCardView.swift +++ b/supacode/Features/Repositories/Views/TerminalPersistenceOnboardingCardView.swift @@ -1,4 +1,5 @@ import Sharing +import SupacodeSettingsShared import SwiftUI /// Bottom-of-sidebar onboarding card announcing zmx-backed session persistence. @@ -41,14 +42,14 @@ private struct TerminalPersistenceOnboardingCardBody: View { VStack(alignment: .leading, spacing: 4) { SidebarCardLabel(title: "Sessions persist across quits", description: description) Text("Manage in Settings → General") - .font(.caption2) + .appFont(.caption2) .foregroundStyle(.tertiary) .padding(.top, 2) } }, header: { Image(systemName: "infinity") - .font(.title2) + .appFont(.title2) .foregroundStyle(.purple) .accessibilityHidden(true) } diff --git a/supacode/Features/Repositories/Views/WorktreeCreationPromptView.swift b/supacode/Features/Repositories/Views/WorktreeCreationPromptView.swift index 7ad862b1c..9c6e185c0 100644 --- a/supacode/Features/Repositories/Views/WorktreeCreationPromptView.swift +++ b/supacode/Features/Repositories/Views/WorktreeCreationPromptView.swift @@ -185,7 +185,7 @@ private struct WorktreeRefPickerField: View { VStack(alignment: .leading, spacing: 2) { Text(title) Text(caption) - .font(.caption) + .appFont(.caption) .foregroundStyle(.secondary) } HStack(spacing: 8) { @@ -300,7 +300,7 @@ private struct WorktreeRefFilterResults: View { var body: some View { if matches.isEmpty { Text("No matching branches") - .font(.callout) + .appFont(.callout) .foregroundStyle(.secondary) } else { VStack(alignment: .leading, spacing: 0) { @@ -320,7 +320,7 @@ private struct WorktreeRefFilterResults: View { .padding(.horizontal, -4) if total > matches.count { Text("\(rangeStart) to \(rangeEnd), out of \(total)") - .font(.caption) + .appFont(.caption) .foregroundStyle(.secondary) .padding(.top, 2) } @@ -350,7 +350,7 @@ private struct WorktreeRefResultRow: View { .truncationMode(.middle) Spacer(minLength: 8) Text(display.scope) - .font(.caption) + .appFont(.caption) .foregroundStyle(.secondary) } .padding(.vertical, 3) diff --git a/supacode/Features/Repositories/Views/WorktreeDetailTitleView.swift b/supacode/Features/Repositories/Views/WorktreeDetailTitleView.swift index c8f2de4fb..a947f8906 100644 --- a/supacode/Features/Repositories/Views/WorktreeDetailTitleView.swift +++ b/supacode/Features/Repositories/Views/WorktreeDetailTitleView.swift @@ -126,7 +126,7 @@ struct WorktreeToolbarTitleView: View { case .folder(let name, let tint, let hostInfo): HStack(spacing: 4) { Text(name) - .font(.callout.weight(.semibold)) + .appFont(.callout, weight: .semibold) .foregroundStyle(tint?.color ?? .primary) .lineLimit(1) .truncationMode(.middle) @@ -140,7 +140,7 @@ struct WorktreeToolbarTitleView: View { } case .git(let payload): Text(payload.displayTitle) - .font(.callout.weight(.semibold)) + .appFont(.callout, weight: .semibold) .foregroundStyle(payload.worktreeTint?.color ?? .primary) .lineLimit(1) .truncationMode(.middle) @@ -164,7 +164,7 @@ struct WorktreeToolbarTitleView: View { trail } } - .font(.footnote) + .appFont(.footnote) .lineLimit(1) } } diff --git a/supacode/Features/Repositories/Views/WorktreeDetailView.swift b/supacode/Features/Repositories/Views/WorktreeDetailView.swift index 25beb0283..617569347 100644 --- a/supacode/Features/Repositories/Views/WorktreeDetailView.swift +++ b/supacode/Features/Repositories/Views/WorktreeDetailView.swift @@ -640,6 +640,11 @@ struct WorktreeDetailView: View { ToolbarItem(placement: .navigation) { TerminalSchemeHost(scheme: scheme) { WorktreeToolbarTitleView(content: toolbarState.titleContent) + // `TerminalSchemeHost` re-hosts its content in a fresh + // `NSHostingView`, which starts a new environment rather than + // inheriting the window's. Publish the size inside the closure so it + // travels with the content value. + .appChromeTextSize(settingsFile.global.chromeTextSize) } } .sharedBackgroundVisibility(.hidden) @@ -1014,7 +1019,7 @@ private struct DetailPlaceholderView: View { ProgressView() .controlSize(.large) Text(Self.messages[messageIndex]) - .font(.title3) + .appFont(.title3) .foregroundStyle(.secondary) .contentTransition(.numericText()) .shimmer(isActive: true) @@ -1056,7 +1061,7 @@ private struct ToolbarPlaceholderContent: ToolbarContent { .foregroundStyle(.secondary) Text("feature/branch") } - .font(.headline) + .appFont(.headline) } .redacted(reason: .placeholder) .shimmer(isActive: true) @@ -1135,7 +1140,7 @@ private struct MultiSelectedWorktreesDetailView: View { let deleteShortcut = KeyboardShortcut(.delete, modifiers: [.command, .shift]).display VStack(alignment: .leading, spacing: 20) { Text("\(rows.count) items selected") - .font(.title3) + .appFont(.title3) if !worktreeRows.isEmpty { selectionSection( @@ -1167,12 +1172,12 @@ private struct MultiSelectedWorktreesDetailView: View { if isMixedKindSelection { VStack(alignment: .leading, spacing: 6) { Label("No bulk action available", systemImage: "exclamationmark.triangle") - .font(.headline) + .appFont(.headline) Text( "Worktrees and folders don't share bulk actions. Deselect " + "one kind to archive/delete worktrees or remove folders." ) - .font(.caption) + .appFont(.caption) .foregroundStyle(.secondary) } } @@ -1191,7 +1196,7 @@ private struct MultiSelectedWorktreesDetailView: View { ) -> some View { VStack(alignment: .leading, spacing: 8) { Text(title) - .font(.headline) + .appFont(.headline) ForEach(Array(rows.prefix(visibleRowsLimit))) { row in HStack(alignment: .firstTextBaseline, spacing: 8) { Text(row.name) @@ -1202,23 +1207,23 @@ private struct MultiSelectedWorktreesDetailView: View { .lineLimit(1) } } - .font(.body) + .appFont(.body) } if rows.count > visibleRowsLimit { Text("+\(rows.count - visibleRowsLimit) more") - .font(.caption) + .appFont(.caption) .foregroundStyle(.secondary) } if !actions.isEmpty { VStack(alignment: .leading, spacing: 4) { Text("Available actions") - .font(.subheadline) + .appFont(.subheadline) .foregroundStyle(.secondary) ForEach(actions, id: \.self) { action in Text(action) } } - .font(.caption) + .appFont(.caption) .foregroundStyle(.secondary) .padding(.top, 4) } diff --git a/supacode/Features/Repositories/Views/WorktreeLoadingView.swift b/supacode/Features/Repositories/Views/WorktreeLoadingView.swift index dd69e04eb..5460b5ba7 100644 --- a/supacode/Features/Repositories/Views/WorktreeLoadingView.swift +++ b/supacode/Features/Repositories/Views/WorktreeLoadingView.swift @@ -1,3 +1,4 @@ +import SupacodeSettingsShared import SwiftUI struct WorktreeLoadingView: View { @@ -10,17 +11,17 @@ struct WorktreeLoadingView: View { .controlSize(.large) VStack(spacing: 4) { Text(info.name) - .font(.title3) + .appFont(.title3) if let command = info.progress?.statusCommand { Text(command) - .font(.subheadline) + .appFont(.subheadline) .monospaced() .foregroundStyle(.secondary) .lineLimit(1) .truncationMode(.middle) } Text(subtitle) - .font(.subheadline) + .appFont(.subheadline) .monospaced() .foregroundStyle(.tertiary) .lineLimit(5, reservesSpace: true) diff --git a/supacode/Features/Repositories/Views/WorktreeStatusInspector.swift b/supacode/Features/Repositories/Views/WorktreeStatusInspector.swift index eec0a7640..d58b6b4a1 100644 --- a/supacode/Features/Repositories/Views/WorktreeStatusInspector.swift +++ b/supacode/Features/Repositories/Views/WorktreeStatusInspector.swift @@ -119,7 +119,7 @@ private struct GitInspectorContent: View { VStack(spacing: 0) { HStack { Text("Pull Request") - .font(.headline) + .appFont(.headline) Spacer() if let url { Button { @@ -167,12 +167,12 @@ private struct GitInspectorContent: View { .foregroundStyle(.secondary) } Text(pullRequest.title) - .font(.headline) + .appFont(.headline) .textSelection(.enabled) Text( "`\(pullRequest.baseRefName ?? "base")` ← `\(pullRequest.headRefName ?? "branch")`" ) - .font(.subheadline) + .appFont(.subheadline) .monospaced() .foregroundStyle(.secondary) } @@ -199,7 +199,7 @@ private struct GitInspectorContent: View { PullRequestChecksRingView(breakdown: breakdown) Text(breakdown.summaryText) .foregroundStyle(.secondary) - .font(.callout) + .appFont(.callout) } ForEach(sortedChecks, id: \.self) { check in CheckRow(check: check) @@ -370,7 +370,7 @@ private struct CheckRowLabel: View { .lineLimit(1) Spacer() Text(style.label) - .font(.caption) + .appFont(.caption) .foregroundStyle(.secondary) } } @@ -393,7 +393,7 @@ private struct PullRequestMergeQueueRow: View { } if let detail = status.detail { Text(detail) - .font(.caption) + .appFont(.caption) .foregroundStyle(.secondary) } } @@ -442,7 +442,7 @@ private struct NotificationsInspectorContent: View { VStack(spacing: 0) { HStack { Text("Notifications") - .font(.headline) + .appFont(.headline) Spacer() Button("Dismiss All", action: onDismissAll) .buttonStyle(.borderless) @@ -535,7 +535,7 @@ private struct NotificationWorktreeHeader: View { .foregroundStyle(.secondary) } } - .font(.subheadline.weight(.medium)) + .appFont(.subheadline, weight: .medium) .lineLimit(1) .textCase(nil) } @@ -564,14 +564,14 @@ private struct NotificationRow: View { VStack(alignment: .leading, spacing: 2) { HStack(alignment: .firstTextBaseline, spacing: 6) { Text(title) - .font(.subheadline.weight(.semibold)) + .appFont(.subheadline, weight: .semibold) .foregroundStyle(notification.isRead ? Color.secondary : Color.primary) .lineLimit(1) Spacer(minLength: 6) // Self-updating relative time; no shared clock needed, so a row's // markdown body is never re-parsed just to advance the timestamp. Text(notification.createdAt, style: .relative) - .font(.caption) + .appFont(.caption) .foregroundStyle(.tertiary) .lineLimit(1) .fixedSize() @@ -583,7 +583,7 @@ private struct NotificationRow: View { } if !notification.body.isEmpty { Text(Self.markdown(notification.body)) - .font(.callout) + .appFont(.callout) .foregroundStyle(notification.isRead ? Color.secondary : Color.primary) .fixedSize(horizontal: false, vertical: true) .frame(maxWidth: .infinity, alignment: .leading) @@ -622,7 +622,7 @@ private struct PrunedNotificationRow: View { VStack(alignment: .leading, spacing: 2) { HStack(alignment: .firstTextBaseline, spacing: 6) { Text(title) - .font(.subheadline.weight(.semibold)) + .appFont(.subheadline, weight: .semibold) .foregroundStyle(.primary) .lineLimit(1) Spacer(minLength: 6) @@ -632,7 +632,7 @@ private struct PrunedNotificationRow: View { .accessibilityHidden(true) } Text("Cleared per your Notification settings.") - .font(.callout) + .appFont(.callout) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) .frame(maxWidth: .infinity, alignment: .leading) @@ -661,7 +661,7 @@ private struct NotificationSourceIcon: View { AgentBadgeView(agent: agent, size: 22) } else { Image(systemName: "bell.fill") - .font(.caption2) + .appFont(.caption2) .foregroundStyle(.secondary) .frame(width: 22, height: 22) .background(.bar, in: .circle) diff --git a/supacode/Features/Settings/Views/DeveloperSettingsView.swift b/supacode/Features/Settings/Views/DeveloperSettingsView.swift index d906e64b4..b90eac01e 100644 --- a/supacode/Features/Settings/Views/DeveloperSettingsView.swift +++ b/supacode/Features/Settings/Views/DeveloperSettingsView.swift @@ -102,7 +102,7 @@ private struct AgentInstallPromptRow: View { VStack(alignment: .leading, spacing: 2) { Text("Agent integrations") Text(subtitle) - .font(.subheadline) + .appFont(.subheadline) .foregroundStyle(.secondary) } } @@ -256,10 +256,10 @@ private struct AgentIntegrationRow: View { VStack(alignment: .leading, spacing: 2) { Text(agent.displayName) Text(agent.integrationSubtitle) - .font(.subheadline) + .appFont(.subheadline) .foregroundStyle(.secondary) if let message = state.errorMessage { - Text(message).font(.subheadline).foregroundStyle(.red) + Text(message).appFont(.subheadline).foregroundStyle(.red) } } Spacer() diff --git a/supacode/Features/Settings/Views/GithubSettingsView.swift b/supacode/Features/Settings/Views/GithubSettingsView.swift index b1d3fbf45..94f18079a 100644 --- a/supacode/Features/Settings/Views/GithubSettingsView.swift +++ b/supacode/Features/Settings/Views/GithubSettingsView.swift @@ -78,7 +78,7 @@ struct GithubSettingsView: View { Text("GitHub CLI not found") Text("Install `gh` to enable pull request checks.") .foregroundStyle(.secondary) - .font(.callout) + .appFont(.callout) } } icon: { Image(systemName: "xmark.circle") @@ -92,7 +92,7 @@ struct GithubSettingsView: View { Text("Not authenticated") Text("Run `gh auth login` in a terminal to authenticate.") .foregroundStyle(.secondary) - .font(.callout) + .appFont(.callout) } } icon: { Image(systemName: "exclamationmark.triangle") @@ -106,7 +106,7 @@ struct GithubSettingsView: View { Text("GitHub CLI outdated") Text("Update to the latest version for full support.") .foregroundStyle(.secondary) - .font(.callout) + .appFont(.callout) } } icon: { Image(systemName: "exclamationmark.triangle") @@ -128,7 +128,7 @@ struct GithubSettingsView: View { Text("Error checking status") Text(message) .foregroundStyle(.secondary) - .font(.callout) + .appFont(.callout) } } icon: { Image(systemName: "exclamationmark.triangle") diff --git a/supacode/Features/Settings/Views/HotkeyRecorderView.swift b/supacode/Features/Settings/Views/HotkeyRecorderView.swift index 4bc22153d..6528bdaee 100644 --- a/supacode/Features/Settings/Views/HotkeyRecorderView.swift +++ b/supacode/Features/Settings/Views/HotkeyRecorderView.swift @@ -9,7 +9,7 @@ struct Keycap: View { var body: some View { Text(symbol) - .font(.body.weight(.medium).monospaced()) + .appFont(.body, weight: .medium, monospaced: true) .padding(.horizontal, 6) .frame(minWidth: 28, minHeight: 28) .background(.quaternary, in: .rect(cornerRadius: 6)) @@ -43,13 +43,13 @@ struct HotkeyRecorderPopover: View { Image(systemName: "checkmark.circle.fill") .accessibilityHidden(true) } - .font(.caption) + .appFont(.caption) .foregroundStyle(.green) case .conflict(let override, let name): KeycapsView(override: override) Text("Already used by \(name).") - .font(.caption) + .appFont(.caption) .foregroundStyle(.red) .fixedSize(horizontal: true, vertical: false) @@ -72,7 +72,7 @@ struct HotkeyRecorderPopover: View { } .frame(minHeight: 28) Text("Recording…") - .font(.caption) + .appFont(.caption) .foregroundStyle(.secondary) } } @@ -87,7 +87,7 @@ struct HotkeyRecorderPopover: View { onCancelled() } label: { Image(systemName: "xmark") - .font(.caption2) + .appFont(.caption2) .foregroundStyle(.secondary) .accessibilityLabel("Cancel") } diff --git a/supacode/Features/Settings/Views/SettingsView.swift b/supacode/Features/Settings/Views/SettingsView.swift index 7a8af9417..a11e988df 100644 --- a/supacode/Features/Settings/Views/SettingsView.swift +++ b/supacode/Features/Settings/Views/SettingsView.swift @@ -128,6 +128,8 @@ private struct SettingsSidebarView: View { List(selection: $settingsStore.selection.sending(\.setSelection)) { Label("General", systemImage: "gearshape") .tag(SettingsSection.general) + Label("Accessibility", systemImage: "accessibility") + .tag(SettingsSection.accessibility) Label("Notifications", systemImage: "bell") .tag(SettingsSection.notifications) Label("Worktrees", systemImage: "list.dash") @@ -186,6 +188,8 @@ private struct SettingsDetailView: View { switch selection { case .general: AppearanceSettingsView(store: settingsStore) + case .accessibility: + AccessibilitySettingsView(store: settingsStore) case .notifications: NotificationsSettingsView(store: settingsStore) case .worktree: diff --git a/supacode/Features/Settings/Views/ShortcutRowView.swift b/supacode/Features/Settings/Views/ShortcutRowView.swift index 740fc37f1..81ab8fad1 100644 --- a/supacode/Features/Settings/Views/ShortcutRowView.swift +++ b/supacode/Features/Settings/Views/ShortcutRowView.swift @@ -29,7 +29,7 @@ struct HotkeyCellView: View { .foregroundStyle(isModified ? .primary : .secondary) if let warning { Image(systemName: "exclamationmark.triangle.fill") - .font(.caption2) + .appFont(.caption2) .foregroundStyle(.yellow) .accessibilityLabel("Warning") .help(warning) diff --git a/supacode/Features/Terminal/TabBar/Views/TerminalTabLabelView.swift b/supacode/Features/Terminal/TabBar/Views/TerminalTabLabelView.swift index d0414a9d0..dbec62b3e 100644 --- a/supacode/Features/Terminal/TabBar/Views/TerminalTabLabelView.swift +++ b/supacode/Features/Terminal/TabBar/Views/TerminalTabLabelView.swift @@ -46,7 +46,7 @@ private struct TerminalTabTitleLabel: View, Equatable { var body: some View { Text(title) - .font(.caption) + .appFont(.caption) .fontWeight(isActive ? .semibold : .regular) .lineLimit(1) .foregroundStyle(TerminalTabBarColors.activeText) diff --git a/supacode/Features/Terminal/TabBar/Views/TerminalTabView.swift b/supacode/Features/Terminal/TabBar/Views/TerminalTabView.swift index 0bcb242bf..0226ea219 100644 --- a/supacode/Features/Terminal/TabBar/Views/TerminalTabView.swift +++ b/supacode/Features/Terminal/TabBar/Views/TerminalTabView.swift @@ -115,7 +115,7 @@ struct TerminalTabView: View { if isEditing { TextField("", text: $editingTitle) .textFieldStyle(.plain) - .font(.caption) + .appFont(.caption) .focused($isFieldFocused) .foregroundStyle(TerminalTabBarColors.activeText) .accessibilityLabel("Rename tab") @@ -283,10 +283,9 @@ private struct TerminalTabShortcutHintText: View { var body: some View { Text(hint) - .font(.caption) // Explicit `.regular` because the tab bar lacks the sidebar's List/vibrancy - // context, where `.font(.caption)` would otherwise render heavier. - .fontWeight(.regular) + // context, where `.caption` would otherwise render heavier. + .appFont(.caption, weight: .regular) .foregroundStyle(.secondary) .lineLimit(1) .fixedSize() diff --git a/supacode/Features/Terminal/Views/EmptyTerminalPaneView.swift b/supacode/Features/Terminal/Views/EmptyTerminalPaneView.swift index 9e4402945..1d0c16563 100644 --- a/supacode/Features/Terminal/Views/EmptyTerminalPaneView.swift +++ b/supacode/Features/Terminal/Views/EmptyTerminalPaneView.swift @@ -1,3 +1,4 @@ +import SupacodeSettingsShared import SwiftUI struct EmptyTerminalPaneView: View { @@ -6,15 +7,15 @@ struct EmptyTerminalPaneView: View { var body: some View { VStack(spacing: 12) { Image(systemName: "apple.terminal.on.rectangle") - .font(.title) + .appFont(.title) .imageScale(.large) .accessibilityHidden(true) .foregroundStyle(.secondary) VStack(spacing: 4) { Text(message) - .font(.title3) + .appFont(.title3) Text("Use the \(Text("+").bold()) button to open a terminal.") - .font(.subheadline) + .appFont(.subheadline) .foregroundStyle(.secondary) } } diff --git a/supacode/Support/SidebarCardView.swift b/supacode/Support/SidebarCardView.swift index 194487f43..8c4d03d75 100644 --- a/supacode/Support/SidebarCardView.swift +++ b/supacode/Support/SidebarCardView.swift @@ -1,3 +1,4 @@ +import SupacodeSettingsShared import SwiftUI /// Pinned sidebar card surface (glass background, 10pt radius, leading-aligned). @@ -30,7 +31,7 @@ struct SidebarCard: View { onDismiss() } label: { Image(systemName: "xmark") - .font(.caption2) + .appFont(.caption2) .foregroundStyle(.secondary) .frame(width: 18, height: 18) .contentShape(.rect) @@ -64,11 +65,11 @@ struct SidebarCardLabel: View { var body: some View { VStack(alignment: .leading, spacing: 2) { Text(title) - .font(.subheadline) + .appFont(.subheadline) .fontWeight(.semibold) if let description { Text(description) - .font(.caption) + .appFont(.caption) .foregroundStyle(.secondary) } } diff --git a/supacodeTests/ChromeTextSizeTests.swift b/supacodeTests/ChromeTextSizeTests.swift new file mode 100644 index 000000000..51348cbf3 --- /dev/null +++ b/supacodeTests/ChromeTextSizeTests.swift @@ -0,0 +1,31 @@ +import Testing + +@testable import SupacodeSettingsShared + +struct ChromeTextSizeTests { + @Test func defaultIsTheUnmodifiedSystemSize() { + #expect(ChromeTextSize.default == .standard) + #expect(ChromeTextSize.default.scale == 1.0) + } + + @Test func sizesGrowMonotonically() { + let scales = ChromeTextSize.allCases.map(\.scale) + #expect(scales == scales.sorted()) + #expect(Set(scales).count == scales.count) + } + + @Test func casesAreOrderedSmallestFirst() { + // `allCases` is the order the picker renders, so reordering the cases + // reorders the control. + #expect(ChromeTextSize.allCases == [.standard, .large, .extraLarge]) + #expect(ChromeTextSize.allCases.first == .default) + } + + @Test func rawValuesAreStableAcrossReleases() { + // The raw values are the on-disk representation in the settings file; + // renaming one silently resets a user's chosen size back to the default. + #expect(ChromeTextSize.standard.rawValue == "standard") + #expect(ChromeTextSize.large.rawValue == "large") + #expect(ChromeTextSize.extraLarge.rawValue == "extraLarge") + } +} diff --git a/supacodeTests/SettingsFeatureTests.swift b/supacodeTests/SettingsFeatureTests.swift index 83866ec23..f708db014 100644 --- a/supacodeTests/SettingsFeatureTests.swift +++ b/supacodeTests/SettingsFeatureTests.swift @@ -130,6 +130,41 @@ struct SettingsFeatureTests { #expect(settingsFile.global.terminalHibernationEnabled == false) } + @Test(.dependencies) func chromeTextSizePersistsChanges() async { + @Shared(.settingsFile) var settingsFile + $settingsFile.withLock { $0.global = .default } + + let store = TestStore(initialState: SettingsFeature.State()) { + SettingsFeature() + } + + await store.send(.binding(.set(\.chromeTextSize, .extraLarge))) { + $0.chromeTextSize = .extraLarge + } + await store.receive(\.delegate.settingsChanged) + #expect(settingsFile.global.chromeTextSize == .extraLarge) + } + + @Test(.dependencies) func unrelatedSettingsChangeKeepsChromeTextSize() async { + // `persist` assigns `$0.global` wholesale, so a field missing from this + // feature's state would be written back as its default on every unrelated + // change. + var initialSettings = GlobalSettings.default + initialSettings.chromeTextSize = .large + @Shared(.settingsFile) var settingsFile + $settingsFile.withLock { $0.global = initialSettings } + + let store = TestStore(initialState: SettingsFeature.State(settings: initialSettings)) { + SettingsFeature() + } + + await store.send(.binding(.set(\.terminalHibernationEnabled, false))) { + $0.terminalHibernationEnabled = false + } + await store.receive(\.delegate.settingsChanged) + #expect(settingsFile.global.chromeTextSize == .large) + } + @Test(.dependencies) func confirmCloseSurfacePersistsChanges() async { var initialSettings = GlobalSettings.default initialSettings.confirmCloseSurface = true diff --git a/supacodeTests/SettingsFilePersistenceTests.swift b/supacodeTests/SettingsFilePersistenceTests.swift index 5531839cc..b2b9a205c 100644 --- a/supacodeTests/SettingsFilePersistenceTests.swift +++ b/supacodeTests/SettingsFilePersistenceTests.swift @@ -631,6 +631,70 @@ struct SettingsFilePersistenceTests { #expect(settings.global.systemNotificationsEnabled == true) } + @Test(.dependencies) func decodesMissingChromeTextSizeAsDefault() throws { + // A file predating the accessibility text size has no `chromeTextSize` key + // and must migrate to the system default size rather than failing to load. + let legacy = LegacySettingsFile( + global: LegacyGlobalSettings( + appearanceMode: .dark, + updatesAutomaticallyCheckForUpdates: false, + updatesAutomaticallyDownloadUpdates: true + ), + repositories: [:] + ) + let data = try JSONEncoder().encode(legacy) + let storage = MutableTestStorage(initialData: data) + + let settings: SettingsFile = withDependencies { + $0.settingsFileStorage = storage.storage + } operation: { + @Shared(.settingsFile) var settings: SettingsFile + return settings + } + + #expect(settings.global.chromeTextSize == .default) + } + + @Test(.dependencies) func decodesUnrecognizedChromeTextSizeAsDefaultWithoutDiscardingTheFile() throws { + // An unknown size (older build, hand-edit) must fall back by itself rather + // than throwing, which would reset every other setting in the file. + let json = """ + {"global":{"appearanceMode":"light","updatesAutomaticallyCheckForUpdates":false,\ + "updatesAutomaticallyDownloadUpdates":false,"chromeTextSize":"gigantic"},"repositories":{}} + """ + let storage = MutableTestStorage(initialData: Data(json.utf8)) + + let settings: SettingsFile = withDependencies { + $0.settingsFileStorage = storage.storage + } operation: { + @Shared(.settingsFile) var settings: SettingsFile + return settings + } + + #expect(settings.global.chromeTextSize == .default) + #expect(settings.global.appearanceMode == .light) + } + + @Test(.dependencies) func roundTripsExplicitChromeTextSize() throws { + let storage = SettingsTestStorage() + + withDependencies { + $0.settingsFileStorage = storage.storage + } operation: { + @Shared(.settingsFile) var settings: SettingsFile + $settings.withLock { $0.global.chromeTextSize = .extraLarge } + } + + let reloaded: SettingsFile = withDependencies { + $0.settingsFileStorage = storage.storage + } operation: { + @Shared(.settingsFile) var reloaded: SettingsFile + return reloaded + } + + #expect(reloaded.global.chromeTextSize == .extraLarge) + } + @Test(.dependencies) func roundTripsExplicitNotificationRetentionLimit() throws { let storage = SettingsTestStorage()