From 45ec47f0ddd095f01452b33b7e510943c07b6fbb Mon Sep 17 00:00:00 2001 From: Jacob Fu <141651335+FuJacob@users.noreply.github.com> Date: Fri, 12 Jun 2026 08:40:24 -0700 Subject: [PATCH] feat(ui): align Settings styling with the redesigned onboarding The onboarding redesign (#701) and the Settings revamp (#699) landed in parallel with convergent but not identical vocabularies. This closes the gaps so a user who finishes onboarding lands in Settings that speak the same language: - Promote the brand palette to CotabbyBrand (shared file); onboarding's OnboardingTheme references rename, and the Settings Home hero logo now casts the same brand-blue glow as the onboarding welcome hero - Promote the physical keycap to a shared KeycapView and render it in the Shortcuts pane's KeybindRow, replacing the flat quaternary pill, so a binding looks like the same object on both surfaces - Permission rows in Settings derive their glyph from CotabbyPermissionKind.systemImageName instead of hardcoding divergent symbols, matching onboarding and the permission reminder - Add SuggestionEngineKind.systemImageName (one canonical engine glyph: apple.logo / cpu.fill) and use it in the Home status card and onboarding's engine selector and tier-card footers, which previously disagreed (cpu.fill vs internaldrive) --- Cotabby.xcodeproj/project.pbxproj | 12 +++++ Cotabby/Models/SuggestionEngineModels.swift | 12 +++++ Cotabby/UI/CotabbyBrand.swift | 16 ++++++ Cotabby/UI/KeycapView.swift | 45 +++++++++++++++++ Cotabby/UI/Onboarding/OnboardingStyle.swift | 35 ++++++------- Cotabby/UI/Onboarding/WelcomeHeroDemo.swift | 6 +-- .../Onboarding/WelcomeKeybindStepView.swift | 49 ++----------------- .../WelcomePermissionStepView.swift | 4 +- .../WelcomePersonalizeStepView.swift | 2 +- .../Onboarding/WelcomeTemplateStepView.swift | 30 ++++++------ Cotabby/UI/Onboarding/WelcomeView.swift | 12 ++--- Cotabby/UI/Settings/Panes/HomePaneView.swift | 9 ++-- .../Settings/Panes/PermissionsPaneView.swift | 12 +++-- .../UI/Settings/Panes/ShortcutsPaneView.swift | 10 ++-- .../SuggestionEngineModelsTests.swift | 7 +++ 15 files changed, 151 insertions(+), 110 deletions(-) create mode 100644 Cotabby/UI/CotabbyBrand.swift create mode 100644 Cotabby/UI/KeycapView.swift diff --git a/Cotabby.xcodeproj/project.pbxproj b/Cotabby.xcodeproj/project.pbxproj index ebfe417d..370b6645 100644 --- a/Cotabby.xcodeproj/project.pbxproj +++ b/Cotabby.xcodeproj/project.pbxproj @@ -212,6 +212,7 @@ 467E2EF8E7B1EC83F60F6A35 /* EmojiRecents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E72A3972E15749337539C2D /* EmojiRecents.swift */; }; 469626C2EFEDFD6C188941F5 /* de.txt in Resources */ = {isa = PBXBuildFile; fileRef = C648EBB10D7F8E0B904DEC91 /* de.txt */; }; 46F341472191BC451B6BF6B5 /* SuggestionRequestFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE858CB1E687E3CEB8FDD5B /* SuggestionRequestFactory.swift */; }; + 4701AB8E93FA78B808824AAD /* CotabbyBrand.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9B27E3D3FD8008CE2C1B616 /* CotabbyBrand.swift */; }; 47364583344BEA8FDC7178D8 /* DownloadFileRescuer.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B4A2E2DD6733658EC05BD8 /* DownloadFileRescuer.swift */; }; 475FB7450EEC3C1B16E66CC4 /* LLMIOFileHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9030FAAB468119A0236284A6 /* LLMIOFileHandlerTests.swift */; }; 47654BDCFD2DE6D4DE85D7FE /* LanguageTagsEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A7CDA90E128350BFF1A9D66 /* LanguageTagsEditor.swift */; }; @@ -306,6 +307,7 @@ 64A36D1EE1BF29AF5B58906A /* TrailingDuplicationFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D408D647412C59F3E692C42B /* TrailingDuplicationFilter.swift */; }; 64C76BE489BD6E7D1DB94A85 /* CompletionSeamGuard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 306A36888B5F2CA8FEF9C806 /* CompletionSeamGuard.swift */; }; 64DA031AEAC20AC6C852A24A /* OnboardingTemplateFeatureList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24F613F0E2F7046E6532A09C /* OnboardingTemplateFeatureList.swift */; }; + 64F20D51F7905DC290AE0351 /* KeycapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB429709A7FA9D63B0D7B133 /* KeycapView.swift */; }; 6503E1585D0CDE8CD852144B /* MacroTriggerStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C201A65A6B040F90C528A3B /* MacroTriggerStateMachine.swift */; }; 65478B0DABF5460C32D4C458 /* ModelFileValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A829F28F01FAE76CA7244BBC /* ModelFileValidatorTests.swift */; }; 663D37E35292F38666D807A7 /* HuggingFaceModelsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AF9479EF020071CA64CCC1 /* HuggingFaceModelsTests.swift */; }; @@ -433,6 +435,7 @@ 9ADFFF634912F638D079E1C7 /* SentenceBoundaryClassifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4B56C250DDEF3E81F9DCBD7 /* SentenceBoundaryClassifier.swift */; }; 9B0CE2B00695EC13A6E6CCF3 /* CurrencyEvaluator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0537986794554F5FABE6EFF3 /* CurrencyEvaluator.swift */; }; 9BA9E52F33F00E65692EE6CD /* he-100k.txt in Resources */ = {isa = PBXBuildFile; fileRef = 7C9BB65FA5FC42B89766B037 /* he-100k.txt */; }; + 9C5B1ED23126B60E3BBF3A5B /* KeycapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB429709A7FA9D63B0D7B133 /* KeycapView.swift */; }; 9CEBD6AF4405F1BBE0E3D16C /* MidWordContinuationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 357C18383B047F24A531BDCD /* MidWordContinuationPolicy.swift */; }; 9D0F4829D11BCD4DB1290410 /* InsertionStrategySelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0D2FEEA4304C86324BAADAB /* InsertionStrategySelector.swift */; }; 9E031B67A275BB3E049EFC2F /* frequency_dictionary_en_82_765.txt in Resources */ = {isa = PBXBuildFile; fileRef = 99FBB636008490B66CF26772 /* frequency_dictionary_en_82_765.txt */; }; @@ -540,6 +543,7 @@ C423CCD7198ECE27DF260268 /* SurfaceContextComposerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29CDC8BE5312B9BEFD9B22CB /* SurfaceContextComposerTests.swift */; }; C4805FA1A6CD552E572D7012 /* InMemoryLogging in Frameworks */ = {isa = PBXBuildFile; productRef = C42F41C6497D6199DA27D6CD /* InMemoryLogging */; }; C4C6734678797669055988E0 /* AppUpdateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD9573F3504CAE6891DF9B7D /* AppUpdateManager.swift */; }; + C5275D0DA6F5A9D6BFED943D /* CotabbyBrand.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9B27E3D3FD8008CE2C1B616 /* CotabbyBrand.swift */; }; C56ABA04AE27A9943368035C /* CurrentWordSpellChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 733BF6287BDE599B02A12271 /* CurrentWordSpellChecker.swift */; }; C5ADE88B2504F252278E3DD5 /* OverlayController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F308F6E274CC645E27CB651F /* OverlayController.swift */; }; C607A624A0FB697486C56B8E /* PowerSourceMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB235F0DEA53295DAF8B4FA0 /* PowerSourceMonitor.swift */; }; @@ -1050,6 +1054,7 @@ D84D4528EEC9EFEB8AE8E318 /* ActivationIndicatorController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivationIndicatorController.swift; sourceTree = ""; }; D8C2663427BC5219EA415D00 /* ru.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ru.txt; sourceTree = ""; }; D93563FDA25DFC0038E5F887 /* SuggestionSettingsData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionSettingsData.swift; sourceTree = ""; }; + D9B27E3D3FD8008CE2C1B616 /* CotabbyBrand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CotabbyBrand.swift; sourceTree = ""; }; D9C1C921A1CDA2ADFC39EA01 /* AppsPaneView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppsPaneView.swift; sourceTree = ""; }; DB0CE9AB1286367BA2E82392 /* SettingsContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsContainerView.swift; sourceTree = ""; }; DB235F0DEA53295DAF8B4FA0 /* PowerSourceMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PowerSourceMonitor.swift; sourceTree = ""; }; @@ -1095,6 +1100,7 @@ FA4B45B91D4DEAC979C3113E /* PromptContextSanitizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromptContextSanitizer.swift; sourceTree = ""; }; FA878B447441BB4F3E327CC8 /* OnboardingTemplateRecommender.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingTemplateRecommender.swift; sourceTree = ""; }; FB317C82CE2CBC69056BA4B8 /* TagChip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagChip.swift; sourceTree = ""; }; + FB429709A7FA9D63B0D7B133 /* KeycapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeycapView.swift; sourceTree = ""; }; FC24FD54860CE6737E65EF65 /* TextDirectionDetectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextDirectionDetectorTests.swift; sourceTree = ""; }; FC48B188C6E6E263B876621D /* EmojiUsageModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiUsageModels.swift; sourceTree = ""; }; FC83D14A7557BC0196E59007 /* MirrorOverlayLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MirrorOverlayLayoutTests.swift; sourceTree = ""; }; @@ -1603,12 +1609,14 @@ children = ( A2C5A783B8B991CE3F3176CC /* Onboarding */, DCBC24D605EC0CBA1DD6097D /* Settings */, + D9B27E3D3FD8008CE2C1B616 /* CotabbyBrand.swift */, 82F7F7355967725162DF2D1B /* CustomRulesEditor.swift */, BB5C2AE9A7E55495D26AD074 /* DownloadableModelCatalogView.swift */, 3384FD33776960103D6E22A9 /* EmojiPickerView.swift */, CBD5FCB8CC56AA6138382B2C /* FieldEdgeIconIndicatorView.swift */, 78E49BDA7F3A42455C4C5350 /* HuggingFaceModelBrowserView.swift */, 033A468451259A3214EECBE5 /* InlinePreviewView.swift */, + FB429709A7FA9D63B0D7B133 /* KeycapView.swift */, 5A567677424A82D9EEF47495 /* KeyRecorderView.swift */, 9A7CDA90E128350BFF1A9D66 /* LanguageTagsEditor.swift */, 64442042F5B57CB0A701DA85 /* MacroReferenceSheet.swift */, @@ -2013,6 +2021,7 @@ 53480635FD54DAF41B1F5047 /* ControlTokenMarkers.swift in Sources */, B55B160E0534AE23BAC1C3DA /* CotabbyApp.swift in Sources */, AF55F1ABEDEA10C76C307CEC /* CotabbyAppEnvironment.swift in Sources */, + C5275D0DA6F5A9D6BFED943D /* CotabbyBrand.swift in Sources */, 52F3962FA9F424576D7DB5B8 /* CotabbyDebugOptions.swift in Sources */, 9B0CE2B00695EC13A6E6CCF3 /* CurrencyEvaluator.swift in Sources */, B9623395B31459D9D45B1320 /* CurrentWordExtractor.swift in Sources */, @@ -2084,6 +2093,7 @@ 1C267B67EA61527B74C9D051 /* KeyCodeLabels.swift in Sources */, BA74281E2DDE659C5CACBF24 /* KeyRecorderView.swift in Sources */, 4086A1B07488C4D3D43D86C9 /* KeyboardInputSourceMonitor.swift in Sources */, + 64F20D51F7905DC290AE0351 /* KeycapView.swift in Sources */, E3CAAEFAAB5BB24CEE16445B /* LLMIOFileHandler.swift in Sources */, CADCC1B825DBE7BFC3135F43 /* LanguageCatalog.swift in Sources */, 47654BDCFD2DE6D4DE85D7FE /* LanguageTagsEditor.swift in Sources */, @@ -2257,6 +2267,7 @@ 210EEE9D094586286EDD89E8 /* ControlTokenMarkers.swift in Sources */, AA2E09FF7E430D66ECA8ECD5 /* CotabbyApp.swift in Sources */, FCC571EC239846F06007BFCA /* CotabbyAppEnvironment.swift in Sources */, + 4701AB8E93FA78B808824AAD /* CotabbyBrand.swift in Sources */, 0D8241CD31942A25EC4E0EE4 /* CotabbyDebugOptions.swift in Sources */, 862146ABDADC022A3BE74E00 /* CurrencyEvaluator.swift in Sources */, B6703DAE949C7FB034634424 /* CurrentWordExtractor.swift in Sources */, @@ -2328,6 +2339,7 @@ F78F594F77C26C233377E71F /* KeyCodeLabels.swift in Sources */, F8E86FA4D6CEEBFA7FB55F8D /* KeyRecorderView.swift in Sources */, 3124AD2340D4B58AF48A22F3 /* KeyboardInputSourceMonitor.swift in Sources */, + 9C5B1ED23126B60E3BBF3A5B /* KeycapView.swift in Sources */, 046C133967B32BBF9205EBB1 /* LLMIOFileHandler.swift in Sources */, 0A2DDD946654076675AC0FC6 /* LanguageCatalog.swift in Sources */, 51C069603DA16830868F1628 /* LanguageTagsEditor.swift in Sources */, diff --git a/Cotabby/Models/SuggestionEngineModels.swift b/Cotabby/Models/SuggestionEngineModels.swift index a2f155e8..0770f35b 100644 --- a/Cotabby/Models/SuggestionEngineModels.swift +++ b/Cotabby/Models/SuggestionEngineModels.swift @@ -22,6 +22,18 @@ enum SuggestionEngineKind: String, CaseIterable, Equatable, Hashable, Sendable, } } + /// Canonical SF Symbol for the engine, following the `CotabbyPermissionKind.systemImageName` + /// precedent: one source so onboarding, the Settings Home status card, and any future surface + /// render the same engine with the same glyph. + var systemImageName: String { + switch self { + case .appleIntelligence: + return "apple.logo" + case .llamaOpenSource: + return "cpu.fill" + } + } + var supportsLocalModelManagement: Bool { switch self { case .appleIntelligence: diff --git a/Cotabby/UI/CotabbyBrand.swift b/Cotabby/UI/CotabbyBrand.swift new file mode 100644 index 00000000..6ca2b7cd --- /dev/null +++ b/Cotabby/UI/CotabbyBrand.swift @@ -0,0 +1,16 @@ +import SwiftUI + +/// File overview: +/// Cotabby's brand palette, shared by every surface that speaks in the brand voice (onboarding, +/// the permission reminder, and the Settings Home hero). Pinned rather than derived from +/// `Color.accentColor` so brand moments stay on-brand even when the user picks a different system +/// accent; ordinary interactive controls should keep following the system accent. +enum CotabbyBrand { + /// The brand blue, sampled from the app icon's background (#007AFF). Identical in both + /// appearances. + static let accent = Color(red: 0.0, green: 0.478, blue: 1.0) + + /// Lighter companion to `accent`, used as the top stop of icon-tile and pip gradients so + /// tinted elements read as lit from above (the System Settings icon treatment). + static let accentSoft = Color(red: 0.33, green: 0.63, blue: 1.0) +} diff --git a/Cotabby/UI/KeycapView.swift b/Cotabby/UI/KeycapView.swift new file mode 100644 index 00000000..fdcae3c0 --- /dev/null +++ b/Cotabby/UI/KeycapView.swift @@ -0,0 +1,45 @@ +import SwiftUI + +/// File overview: +/// A key binding rendered as a physical keycap: top-lit gradient, hairline edge, and a hard +/// one-point ledge shadow that gives the key its height. Shared by the onboarding keys step and +/// the Settings Shortcuts pane so a binding looks like the same object on both surfaces. Pure +/// chrome; recording flows never enter here. +struct KeycapView: View { + let label: String + var fontSize: CGFloat = 13 + var minWidth: CGFloat = 44 + + @Environment(\.colorScheme) private var colorScheme + + var body: some View { + Text(label) + .font(.system(size: fontSize, weight: .semibold, design: .rounded)) + .foregroundStyle(.primary) + .padding(.horizontal, 12) + .padding(.vertical, 7) + .frame(minWidth: minWidth) + .background( + RoundedRectangle(cornerRadius: 7, style: .continuous) + .fill( + LinearGradient( + colors: colorScheme == .dark + ? [Color(white: 0.26), Color(white: 0.19)] + : [Color.white, Color(white: 0.92)], + startPoint: .top, + endPoint: .bottom + ) + ) + .shadow( + color: .black.opacity(colorScheme == .dark ? 0.55 : 0.22), + radius: 0.5, + y: 1.5 + ) + ) + .overlay( + RoundedRectangle(cornerRadius: 7, style: .continuous) + .strokeBorder(Color.primary.opacity(0.12), lineWidth: 0.5) + ) + .fixedSize() + } +} diff --git a/Cotabby/UI/Onboarding/OnboardingStyle.swift b/Cotabby/UI/Onboarding/OnboardingStyle.swift index 9bab9528..70049384 100644 --- a/Cotabby/UI/Onboarding/OnboardingStyle.swift +++ b/Cotabby/UI/Onboarding/OnboardingStyle.swift @@ -1,26 +1,19 @@ import SwiftUI /// File overview: -/// The shared design system for the first-run onboarding flow: brand palette, window backdrop, -/// icon tiles, card chrome, entrance choreography, and the step header / navigation / progress -/// components every step composes. Keeping the vocabulary in one file is what keeps the six steps -/// reading as one designed surface instead of six separately-styled screens. +/// The shared design system for the first-run onboarding flow: window backdrop, icon tiles, card +/// chrome, entrance choreography, and the step header / navigation / progress components every +/// step composes. Keeping the vocabulary in one file is what keeps the six steps reading as one +/// designed surface instead of six separately-styled screens. The brand palette itself lives in +/// `CotabbyBrand` (shared with Settings' brand moments); the keycap chrome lives in `KeycapView` +/// (shared with the Shortcuts pane). /// /// Two constraints shape everything here: /// 1. Energy. The backdrop and chrome are static; the only continuous animations in onboarding /// are the explicitly-looping product demos, and every entrance effect is a one-shot spring. /// 2. Reduce Motion. Each animated component checks `accessibilityReduceMotion` and collapses to /// its resting frame, mirroring the convention in `OnboardingFeatureShowcase`. -enum OnboardingTheme { - /// Cotabby's brand blue, sampled from the app icon's background (#007AFF). Defined explicitly - /// rather than via `Color.accentColor` so onboarding stays on-brand even when the user has - /// picked a different system accent color, and identical in both appearances. - static let accent = Color(red: 0.0, green: 0.478, blue: 1.0) - - /// Lighter companion to `accent`, used as the top stop of icon-tile gradients so tiles read as - /// lit from above (the System Settings icon treatment). - static let accentSoft = Color(red: 0.33, green: 0.63, blue: 1.0) - +enum OnboardingLayout { /// Horizontal content inset shared by every step so text columns line up across transitions. static let horizontalPadding: CGFloat = 36 } @@ -41,14 +34,14 @@ struct OnboardingBackdrop: View { // Two offset radial washes rather than one centered one: the asymmetry keeps the // gradient from reading as a spotlight and gives the titlebar region gentle color. RadialGradient( - colors: [OnboardingTheme.accent.opacity(colorScheme == .dark ? 0.26 : 0.14), .clear], + colors: [CotabbyBrand.accent.opacity(colorScheme == .dark ? 0.26 : 0.14), .clear], center: UnitPoint(x: 0.15, y: -0.1), startRadius: 10, endRadius: 460 ) RadialGradient( - colors: [OnboardingTheme.accentSoft.opacity(colorScheme == .dark ? 0.16 : 0.10), .clear], + colors: [CotabbyBrand.accentSoft.opacity(colorScheme == .dark ? 0.16 : 0.10), .clear], center: UnitPoint(x: 0.95, y: 0.0), startRadius: 10, endRadius: 420 @@ -159,7 +152,7 @@ extension View { /// secondary subtitle. One component so typography can never drift between steps. struct OnboardingStepHeader: View { var systemImage: String? - var tint: Color = OnboardingTheme.accent + var tint: Color = CotabbyBrand.accent let title: String let subtitle: String @@ -209,14 +202,14 @@ struct OnboardingProgressPips: View { if index == current { return AnyShapeStyle( LinearGradient( - colors: [OnboardingTheme.accentSoft, OnboardingTheme.accent], + colors: [CotabbyBrand.accentSoft, CotabbyBrand.accent], startPoint: .leading, endPoint: .trailing ) ) } if index < current { - return AnyShapeStyle(OnboardingTheme.accent.opacity(0.55)) + return AnyShapeStyle(CotabbyBrand.accent.opacity(0.55)) } return AnyShapeStyle(Color.secondary.opacity(0.22)) } @@ -238,7 +231,7 @@ struct WelcomeButton: View { .padding(.vertical, 2) } .buttonStyle(.borderedProminent) - .tint(OnboardingTheme.accent) + .tint(CotabbyBrand.accent) .controlSize(.large) } } @@ -269,7 +262,7 @@ struct WelcomeNavigation: View { onContinue() } .buttonStyle(.borderedProminent) - .tint(OnboardingTheme.accent) + .tint(CotabbyBrand.accent) .controlSize(.large) .disabled(!canContinue) .help(canContinue ? "" : (disabledHint ?? "")) diff --git a/Cotabby/UI/Onboarding/WelcomeHeroDemo.swift b/Cotabby/UI/Onboarding/WelcomeHeroDemo.swift index 373251d8..998761b3 100644 --- a/Cotabby/UI/Onboarding/WelcomeHeroDemo.swift +++ b/Cotabby/UI/Onboarding/WelcomeHeroDemo.swift @@ -79,7 +79,7 @@ struct WelcomeHeroDemo: View { // The insertion point sits between the typed text and the ghost, exactly where the // real caret stays while a suggestion is displayed. Capsule() - .fill(OnboardingTheme.accent) + .fill(CotabbyBrand.accent) .frame(width: 2, height: 18) .opacity(caretVisible ? 1 : 0) .padding(.horizontal, 1) @@ -109,12 +109,12 @@ struct WelcomeHeroDemo: View { // marks the focused text field. Sells "Cotabby is live in this field right now." .overlay( RoundedRectangle(cornerRadius: 9, style: .continuous) - .strokeBorder(OnboardingTheme.accent.opacity(0.65), lineWidth: 1) + .strokeBorder(CotabbyBrand.accent.opacity(0.65), lineWidth: 1) ) .overlay( RoundedRectangle(cornerRadius: 9, style: .continuous) .inset(by: -2.5) - .strokeBorder(OnboardingTheme.accent.opacity(0.22), lineWidth: 3) + .strokeBorder(CotabbyBrand.accent.opacity(0.22), lineWidth: 3) ) } diff --git a/Cotabby/UI/Onboarding/WelcomeKeybindStepView.swift b/Cotabby/UI/Onboarding/WelcomeKeybindStepView.swift index 6bd37816..c2065051 100644 --- a/Cotabby/UI/Onboarding/WelcomeKeybindStepView.swift +++ b/Cotabby/UI/Onboarding/WelcomeKeybindStepView.swift @@ -120,7 +120,7 @@ struct WelcomeKeybindStepView: View { .font(.system(size: 14, weight: .medium, design: .rounded)) .frame(maxWidth: .infinity, alignment: .leading) - OnboardingKeycap(label: keyLabel) + KeycapView(label: keyLabel) if isRecording { KeyRecorderView( @@ -172,48 +172,7 @@ struct WelcomeKeybindStepView: View { } } -// MARK: - Keycaps - -/// A binding rendered as a physical keycap: top-lit gradient, hairline edge, and a hard one-point -/// ledge shadow that gives the key its height. Pure chrome; the recording flow never enters here. -struct OnboardingKeycap: View { - let label: String - var fontSize: CGFloat = 13 - var minWidth: CGFloat = 44 - - @Environment(\.colorScheme) private var colorScheme - - var body: some View { - Text(label) - .font(.system(size: fontSize, weight: .semibold, design: .rounded)) - .foregroundStyle(.primary) - .padding(.horizontal, 12) - .padding(.vertical, 7) - .frame(minWidth: minWidth) - .background( - RoundedRectangle(cornerRadius: 7, style: .continuous) - .fill( - LinearGradient( - colors: colorScheme == .dark - ? [Color(white: 0.26), Color(white: 0.19)] - : [Color.white, Color(white: 0.92)], - startPoint: .top, - endPoint: .bottom - ) - ) - .shadow( - color: .black.opacity(colorScheme == .dark ? 0.55 : 0.22), - radius: 0.5, - y: 1.5 - ) - ) - .overlay( - RoundedRectangle(cornerRadius: 7, style: .continuous) - .strokeBorder(Color.primary.opacity(0.12), lineWidth: 0.5) - ) - .fixedSize() - } -} +// MARK: - Keycap hero /// The step's hero: the user's current accept key as a large keycap that presses itself every few /// seconds. The press is a two-frame dip (translate down, ledge shadow collapses) driven by a @@ -225,11 +184,11 @@ private struct OnboardingKeycapHero: View { @State private var pressed = false var body: some View { - OnboardingKeycap(label: label, fontSize: 17, minWidth: 84) + KeycapView(label: label, fontSize: 17, minWidth: 84) .scaleEffect(pressed ? 0.96 : 1.0) .offset(y: pressed ? 2 : 0) .shadow( - color: OnboardingTheme.accent.opacity(pressed ? 0.1 : 0.25), + color: CotabbyBrand.accent.opacity(pressed ? 0.1 : 0.25), radius: pressed ? 6 : 14, y: pressed ? 2 : 6 ) diff --git a/Cotabby/UI/Onboarding/WelcomePermissionStepView.swift b/Cotabby/UI/Onboarding/WelcomePermissionStepView.swift index 02baa0b3..85890262 100644 --- a/Cotabby/UI/Onboarding/WelcomePermissionStepView.swift +++ b/Cotabby/UI/Onboarding/WelcomePermissionStepView.swift @@ -88,7 +88,7 @@ extension CotabbyPermissionKind { var onboardingTint: Color { switch self { case .accessibility: - OnboardingTheme.accent + CotabbyBrand.accent case .inputMonitoring: .indigo case .screenRecording: @@ -151,7 +151,7 @@ private struct PermissionCard: View { ) } .buttonStyle(.borderedProminent) - .tint(OnboardingTheme.accent) + .tint(CotabbyBrand.accent) .controlSize(.regular) .background(ScreenFrameReader(frameInScreen: $actionButtonFrame)) .transition(.opacity) diff --git a/Cotabby/UI/Onboarding/WelcomePersonalizeStepView.swift b/Cotabby/UI/Onboarding/WelcomePersonalizeStepView.swift index f3491224..0b91a5f4 100644 --- a/Cotabby/UI/Onboarding/WelcomePersonalizeStepView.swift +++ b/Cotabby/UI/Onboarding/WelcomePersonalizeStepView.swift @@ -53,7 +53,7 @@ struct WelcomePersonalizeStepView: View { HStack(spacing: 7) { Image(systemName: icon) .font(.system(size: 12, weight: .semibold)) - .foregroundStyle(OnboardingTheme.accent) + .foregroundStyle(CotabbyBrand.accent) Text(title) .font(.system(size: 13, weight: .semibold, design: .rounded)) diff --git a/Cotabby/UI/Onboarding/WelcomeTemplateStepView.swift b/Cotabby/UI/Onboarding/WelcomeTemplateStepView.swift index 030f9660..106b2b46 100644 --- a/Cotabby/UI/Onboarding/WelcomeTemplateStepView.swift +++ b/Cotabby/UI/Onboarding/WelcomeTemplateStepView.swift @@ -69,7 +69,7 @@ struct WelcomeTemplateStepView: View { subtitle: appleAvailable ? "Built into macOS" : foundationModelAvailabilityService.userVisibleMessage, - systemImageName: "apple.logo", + systemImageName: SuggestionEngineKind.appleIntelligence.systemImageName, isSelected: selectedEngine == .appleIntelligence, isDisabled: !appleAvailable, onTap: { onSelectEngine(.appleIntelligence) } @@ -78,7 +78,7 @@ struct WelcomeTemplateStepView: View { EngineChoiceCard( title: SuggestionEngineKind.llamaOpenSource.displayLabel, subtitle: "Local models on your Mac", - systemImageName: "internaldrive.fill", + systemImageName: SuggestionEngineKind.llamaOpenSource.systemImageName, isSelected: selectedEngine == .llamaOpenSource, isDisabled: false, onTap: { onSelectEngine(.llamaOpenSource) } @@ -106,7 +106,7 @@ extension OnboardingTemplate { case .quick: .green case .everyday: - OnboardingTheme.accent + CotabbyBrand.accent case .powerful: .purple case .custom: @@ -144,7 +144,7 @@ private struct TemplateCard: View { RoundedRectangle(cornerRadius: 16, style: .continuous) .fill(.regularMaterial) .shadow( - color: isActive ? OnboardingTheme.accent.opacity(0.18) : .black.opacity(0.07), + color: isActive ? CotabbyBrand.accent.opacity(0.18) : .black.opacity(0.07), radius: isActive ? 7 : 3, y: 1 ) @@ -153,14 +153,14 @@ private struct TemplateCard: View { // across the room; the stroke alone is too subtle once three cards are stacked. if isActive { RoundedRectangle(cornerRadius: 16, style: .continuous) - .fill(OnboardingTheme.accent.opacity(0.06)) + .fill(CotabbyBrand.accent.opacity(0.06)) } } ) .overlay( RoundedRectangle(cornerRadius: 16, style: .continuous) .strokeBorder( - isActive ? OnboardingTheme.accent.opacity(0.55) : Color.primary.opacity(0.07), + isActive ? CotabbyBrand.accent.opacity(0.55) : Color.primary.opacity(0.07), lineWidth: isActive ? 1.5 : 0.5 ) ) @@ -201,7 +201,7 @@ private struct TemplateCard: View { if isActive { Image(systemName: "checkmark.circle.fill") .font(.system(size: 19)) - .foregroundStyle(OnboardingTheme.accent) + .foregroundStyle(CotabbyBrand.accent) .transition(.scale(scale: 0.6).combined(with: .opacity)) } } @@ -308,7 +308,7 @@ private struct TemplateCard: View { case .enabled: Image(systemName: "checkmark") .font(.system(size: 10, weight: .semibold)) - .foregroundStyle(OnboardingTheme.accent) + .foregroundStyle(CotabbyBrand.accent) case .disabled: Image(systemName: "minus") .font(.system(size: 10, weight: .semibold)) @@ -321,7 +321,7 @@ private struct TemplateCard: View { } private var engineSymbol: String { - plan.engine == .appleIntelligence ? "apple.logo" : "internaldrive" + plan.engine.systemImageName } private var engineLabel: String { @@ -347,7 +347,7 @@ private struct TemplateCard: View { if let progress { ProgressView(value: progress) .progressViewStyle(.linear) - .tint(OnboardingTheme.accent) + .tint(CotabbyBrand.accent) } else { // No fraction reported yet: fall back to the default (circular) spinner, since // macOS's linear style renders nothing for an indeterminate ProgressView. @@ -392,7 +392,7 @@ private struct EngineChoiceCard: View { HStack(spacing: 8) { Image(systemName: systemImageName) .font(.system(size: 14, weight: .medium)) - .foregroundStyle(isActive ? AnyShapeStyle(OnboardingTheme.accent) : AnyShapeStyle(.secondary)) + .foregroundStyle(isActive ? AnyShapeStyle(CotabbyBrand.accent) : AnyShapeStyle(.secondary)) Text(title) .font(.system(size: 14, weight: .semibold, design: .rounded)) @@ -403,7 +403,7 @@ private struct EngineChoiceCard: View { if isActive { Image(systemName: "checkmark.circle.fill") .font(.system(size: 15)) - .foregroundStyle(OnboardingTheme.accent) + .foregroundStyle(CotabbyBrand.accent) } } @@ -422,14 +422,14 @@ private struct EngineChoiceCard: View { if isActive { RoundedRectangle(cornerRadius: 12, style: .continuous) - .fill(OnboardingTheme.accent.opacity(0.07)) + .fill(CotabbyBrand.accent.opacity(0.07)) } } ) .overlay( RoundedRectangle(cornerRadius: 12, style: .continuous) .strokeBorder( - isActive ? OnboardingTheme.accent.opacity(0.55) : Color.primary.opacity(0.07), + isActive ? CotabbyBrand.accent.opacity(0.55) : Color.primary.opacity(0.07), lineWidth: isActive ? 1.5 : 0.5 ) ) @@ -453,7 +453,7 @@ private struct RecommendedBadge: View { .background( Capsule().fill( LinearGradient( - colors: [OnboardingTheme.accentSoft, OnboardingTheme.accent], + colors: [CotabbyBrand.accentSoft, CotabbyBrand.accent], startPoint: .topLeading, endPoint: .bottomTrailing ) diff --git a/Cotabby/UI/Onboarding/WelcomeView.swift b/Cotabby/UI/Onboarding/WelcomeView.swift index a91aa9aa..3240f3cf 100644 --- a/Cotabby/UI/Onboarding/WelcomeView.swift +++ b/Cotabby/UI/Onboarding/WelcomeView.swift @@ -183,14 +183,14 @@ extension WelcomeView { VStack(spacing: 0) { ScrollView { stepContent - .padding(.horizontal, OnboardingTheme.horizontalPadding) + .padding(.horizontal, OnboardingLayout.horizontalPadding) .padding(.top, 18) .padding(.bottom, 16) .frame(maxWidth: .infinity) } stepFooter - .padding(.horizontal, OnboardingTheme.horizontalPadding) + .padding(.horizontal, OnboardingLayout.horizontalPadding) .padding(.top, 8) .padding(.bottom, 26) } @@ -285,7 +285,7 @@ extension WelcomeView { .scaledToFit() .frame(width: 84, height: 84) .clipShape(RoundedRectangle(cornerRadius: 19, style: .continuous)) - .shadow(color: OnboardingTheme.accent.opacity(0.45), radius: 22, y: 8) + .shadow(color: CotabbyBrand.accent.opacity(0.45), radius: 22, y: 8) .onboardingReveal(0) VStack(spacing: 8) { @@ -332,7 +332,7 @@ private struct WelcomeFeatureChip: View { HStack(spacing: 5) { Image(systemName: systemImage) .font(.system(size: 10, weight: .semibold)) - .foregroundStyle(OnboardingTheme.accent) + .foregroundStyle(CotabbyBrand.accent) Text(label) .font(.system(size: 11, weight: .medium, design: .rounded)) @@ -372,7 +372,7 @@ extension WelcomeView { OnboardingFeatureShowcase() .onboardingReveal(3) } - .padding(.horizontal, OnboardingTheme.horizontalPadding) + .padding(.horizontal, OnboardingLayout.horizontalPadding) .padding(.top, 40) .padding(.bottom, 16) } @@ -384,7 +384,7 @@ extension WelcomeView { onDismiss() } } - .padding(.horizontal, OnboardingTheme.horizontalPadding) + .padding(.horizontal, OnboardingLayout.horizontalPadding) .padding(.top, 8) .padding(.bottom, 26) .onboardingReveal(4) diff --git a/Cotabby/UI/Settings/Panes/HomePaneView.swift b/Cotabby/UI/Settings/Panes/HomePaneView.swift index 37bea373..17fb2375 100644 --- a/Cotabby/UI/Settings/Panes/HomePaneView.swift +++ b/Cotabby/UI/Settings/Panes/HomePaneView.swift @@ -103,7 +103,9 @@ struct HomePaneView: View { .scaledToFit() .frame(width: 64, height: 64) .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous)) - .shadow(color: .black.opacity(0.18), radius: 8, y: 4) + // The brand-blue glow the onboarding welcome hero casts, scaled to this logo size, + // so finishing onboarding and landing on Home reads as one continuous surface. + .shadow(color: CotabbyBrand.accent.opacity(0.4), radius: 16, y: 6) .scaleEffect(hasAppeared ? 1 : 0.9) .opacity(hasAppeared ? 1 : 0) @@ -312,10 +314,7 @@ struct HomePaneView: View { } private var engineSystemImage: String { - switch suggestionSettings.selectedEngine { - case .appleIntelligence: return "apple.logo" - case .llamaOpenSource: return "cpu.fill" - } + suggestionSettings.selectedEngine.systemImageName } private var engineCaption: String { diff --git a/Cotabby/UI/Settings/Panes/PermissionsPaneView.swift b/Cotabby/UI/Settings/Panes/PermissionsPaneView.swift index 49ccffde..0ca4caf1 100644 --- a/Cotabby/UI/Settings/Panes/PermissionsPaneView.swift +++ b/Cotabby/UI/Settings/Panes/PermissionsPaneView.swift @@ -21,7 +21,6 @@ struct PermissionsPaneView: View { permission: .accessibility, description: "Lets Cotabby see which text field has focus and read its contents " + "so it knows what to continue.", - systemImage: "accessibility", granted: permissionManager.accessibilityGranted, permissionGuidanceController: permissionGuidanceController ) @@ -31,7 +30,6 @@ struct PermissionsPaneView: View { permission: .inputMonitoring, description: "Lets Cotabby see your keystrokes so it can detect when to suggest " + "and which key you used to accept.", - systemImage: "keyboard", granted: permissionManager.inputMonitoringGranted, permissionGuidanceController: permissionGuidanceController ) @@ -41,7 +39,6 @@ struct PermissionsPaneView: View { permission: .screenRecording, description: "Optional. Lets Cotabby screenshot the focused window for extra " + "context. Without it, Cotabby runs in Fast Mode using only the text you've typed.", - systemImage: "camera.viewfinder", granted: permissionManager.screenRecordingGranted, permissionGuidanceController: permissionGuidanceController ) @@ -79,7 +76,6 @@ struct PermissionsPaneView: View { private struct SettingsPermissionRow: View { let permission: CotabbyPermissionKind let description: String - let systemImage: String let granted: Bool let permissionGuidanceController: PermissionGuidanceController @@ -87,7 +83,13 @@ private struct SettingsPermissionRow: View { var body: some View { HStack(spacing: 10) { - SettingsRowLabel(title: permission.compactRowTitle, description: description, systemImage: systemImage) + // The glyph comes from the permission model (the same source onboarding and the + // permission reminder render), so the permission looks like one object everywhere. + SettingsRowLabel( + title: permission.compactRowTitle, + description: description, + systemImage: permission.systemImageName + ) Spacer(minLength: 0) // Optional permissions reuse the required rows' styling and read as real permissions; the // "(Optional)" suffix in `compactRowTitle` is what marks them optional, not a separate diff --git a/Cotabby/UI/Settings/Panes/ShortcutsPaneView.swift b/Cotabby/UI/Settings/Panes/ShortcutsPaneView.swift index bc8b6d50..4965d0cf 100644 --- a/Cotabby/UI/Settings/Panes/ShortcutsPaneView.swift +++ b/Cotabby/UI/Settings/Panes/ShortcutsPaneView.swift @@ -159,13 +159,9 @@ private struct KeybindRow: View { var body: some View { HStack(spacing: 8) { - Text(label) - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background( - RoundedRectangle(cornerRadius: 6) - .fill(.quaternary) - ) + // The same physical-keycap chrome the onboarding keys step renders, so a binding looks + // like the same object on both surfaces. + KeycapView(label: label, fontSize: 12, minWidth: 36) if isRecording { KeyRecorderView( diff --git a/CotabbyTests/SuggestionEngineModelsTests.swift b/CotabbyTests/SuggestionEngineModelsTests.swift index 98e634cc..0d3f71b3 100644 --- a/CotabbyTests/SuggestionEngineModelsTests.swift +++ b/CotabbyTests/SuggestionEngineModelsTests.swift @@ -10,6 +10,13 @@ final class SuggestionEngineModelsTests: XCTestCase { XCTAssertEqual(SuggestionEngineKind.llamaOpenSource.displayLabel, "Open Source") } + func test_suggestionEngineKind_systemImageNamesArePinnedSharedGlyphs() { + // Onboarding's engine cards and Settings' Home status card render these; pinning them keeps + // the engine looking like one object across every surface. + XCTAssertEqual(SuggestionEngineKind.appleIntelligence.systemImageName, "apple.logo") + XCTAssertEqual(SuggestionEngineKind.llamaOpenSource.systemImageName, "cpu.fill") + } + func test_suggestionEngineKind_idMatchesRawValueForEveryCase() { XCTAssertEqual(SuggestionEngineKind.allCases.count, 2) for kind in SuggestionEngineKind.allCases {