From c4742a4ba1159516c7f11ed2f3baf399ca742f03 Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Tue, 17 Mar 2026 16:46:10 -0700 Subject: [PATCH] Refine browser import minimal UI --- Resources/Localizable.xcstrings | 187 +++++++++++++ Sources/Panels/BrowserPanel.swift | 24 +- Sources/Panels/BrowserPanelView.swift | 86 +++++- Sources/cmuxApp.swift | 247 ++++++++++++++++++ cmuxTests/BrowserImportMappingTests.swift | 61 +++++ cmuxTests/CmuxWebViewKeyEquivalentTests.swift | 50 ++++ 6 files changed, 648 insertions(+), 7 deletions(-) diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index 0f8f83df..2a7b1e4a 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -805,6 +805,193 @@ } } }, + "debug.menu.browserToolbarButtonSpacing": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Browser Toolbar Button Spacing" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ブラウザーツールバーのボタン間隔" + } + } + } + }, + "debug.menu.browserProfilePopoverDebug": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Browser Profile Popover Debug…" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ブラウザープロファイルポップオーバーのデバッグ…" + } + } + } + }, + "debug.windows.browserProfilePopover.title": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Browser Profile Popover Debug" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ブラウザープロファイルポップオーバーのデバッグ" + } + } + } + }, + "debug.browserProfilePopover.heading": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Browser Profile Popover" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ブラウザープロファイルポップオーバー" + } + } + } + }, + "debug.browserProfilePopover.note": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Tune the profile popover padding live while comparing it against the browser toolbar menu." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ブラウザーツールバーのメニューと見比べながら、プロファイルポップオーバーの余白をライブで調整します。" + } + } + } + }, + "debug.browserProfilePopover.group.padding": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Padding" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "余白" + } + } + } + }, + "debug.browserProfilePopover.label.horizontal": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Horizontal" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "水平" + } + } + } + }, + "debug.browserProfilePopover.label.vertical": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Vertical" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "垂直" + } + } + } + }, + "debug.browserProfilePopover.group.preview": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Preview" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "プレビュー" + } + } + } + }, + "debug.browserProfilePopover.reset": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Reset" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "リセット" + } + } + } + }, + "debug.browserProfilePopover.liveNote": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Changes apply live to the browser profile popover." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "変更はブラウザープロファイルポップオーバーにライブで反映されます。" + } + } + } + }, "debug.devBuildBanner.title": { "extractionState": "manual", "localizations": { diff --git a/Sources/Panels/BrowserPanel.swift b/Sources/Panels/BrowserPanel.swift index dd65fad9..67d9e2d0 100644 --- a/Sources/Panels/BrowserPanel.swift +++ b/Sources/Panels/BrowserPanel.swift @@ -8384,6 +8384,21 @@ final class BrowserDataImportCoordinator { return wizard.runModal() } +#if DEBUG + func debugMakeImportWizardWindow( + browsers: [InstalledBrowserCandidate], + destinationProfiles: [BrowserProfileDefinition]? = nil, + defaultDestinationProfileID: UUID? = nil + ) -> NSWindow { + let wizard = ImportWizardWindowController( + browsers: browsers, + destinationProfiles: destinationProfiles, + defaultDestinationProfileID: defaultDestinationProfileID + ) + return wizard.debugPanelWindow + } +#endif + #if DEBUG private struct CapturedImportSelection: Encodable { struct Entry: Encodable { @@ -8555,6 +8570,10 @@ final class BrowserDataImportCoordinator { return selection } +#if DEBUG + var debugPanelWindow: NSWindow { panel } +#endif + func windowWillClose(_ notification: Notification) { finishModal(with: .cancel) } @@ -8864,7 +8883,9 @@ final class BrowserDataImportCoordinator { sourceProfilesScrollView.contentView.postsBoundsChangedNotifications = true sourceProfilesScrollHeightConstraint = sourceProfilesScrollView.heightAnchor.constraint(equalToConstant: 76) sourceProfilesScrollHeightConstraint?.isActive = true - sourceProfilesScrollView.widthAnchor.constraint(equalTo: sourceProfilesContainer.widthAnchor).isActive = true + let sourceProfilesScrollWidthConstraint = sourceProfilesScrollView.widthAnchor.constraint( + equalTo: sourceProfilesContainer.widthAnchor + ) sourceProfilesHelpLabel.font = NSFont.systemFont(ofSize: 11) sourceProfilesHelpLabel.textColor = .secondaryLabelColor @@ -8882,6 +8903,7 @@ final class BrowserDataImportCoordinator { sourceProfilesContainer.addArrangedSubview(sourceProfilesTitle) sourceProfilesContainer.addArrangedSubview(sourceProfilesScrollView) sourceProfilesContainer.addArrangedSubview(sourceProfilesHelpLabel) + sourceProfilesScrollWidthConstraint.isActive = true sourceProfilesContainer.setHuggingPriority(.defaultLow, for: .vertical) sourceProfilesContainer.setContentCompressionResistancePriority(.defaultLow, for: .vertical) } diff --git a/Sources/Panels/BrowserPanelView.swift b/Sources/Panels/BrowserPanelView.swift index 0fc8446b..136cb802 100644 --- a/Sources/Panels/BrowserPanelView.swift +++ b/Sources/Panels/BrowserPanelView.swift @@ -110,6 +110,45 @@ enum BrowserDevToolsButtonDebugSettings { } } +enum BrowserToolbarAccessorySpacingDebugSettings { + static let key = "browserToolbarAccessorySpacing" + static let defaultSpacing = 2 + static let supportedValues = [0, 2, 4, 6, 8] + + static func resolved(_ rawValue: Int) -> Int { + supportedValues.contains(rawValue) ? rawValue : defaultSpacing + } + + static func current(defaults: UserDefaults = .standard) -> Int { + resolved(defaults.object(forKey: key) as? Int ?? defaultSpacing) + } +} + +enum BrowserProfilePopoverDebugSettings { + static let horizontalPaddingKey = "browserProfilePopoverHorizontalPadding" + static let verticalPaddingKey = "browserProfilePopoverVerticalPadding" + static let defaultHorizontalPadding = 12.0 + static let defaultVerticalPadding = 10.0 + static let horizontalPaddingRange = 8.0...20.0 + static let verticalPaddingRange = 4.0...14.0 + + static func resolvedHorizontalPadding(_ rawValue: Double) -> Double { + horizontalPaddingRange.contains(rawValue) ? rawValue : defaultHorizontalPadding + } + + static func resolvedVerticalPadding(_ rawValue: Double) -> Double { + verticalPaddingRange.contains(rawValue) ? rawValue : defaultVerticalPadding + } + + static func currentHorizontalPadding(defaults: UserDefaults = .standard) -> Double { + resolvedHorizontalPadding((defaults.object(forKey: horizontalPaddingKey) as? NSNumber)?.doubleValue ?? defaultHorizontalPadding) + } + + static func currentVerticalPadding(defaults: UserDefaults = .standard) -> Double { + resolvedVerticalPadding((defaults.object(forKey: verticalPaddingKey) as? NSNumber)?.doubleValue ?? defaultVerticalPadding) + } +} + struct OmnibarInlineCompletion: Equatable { let typedText: String let displayText: String @@ -249,6 +288,11 @@ struct BrowserPanelView: View { @AppStorage(BrowserSearchSettings.searchSuggestionsEnabledKey) private var searchSuggestionsEnabledStorage = BrowserSearchSettings.defaultSearchSuggestionsEnabled @AppStorage(BrowserDevToolsButtonDebugSettings.iconNameKey) private var devToolsIconNameRaw = BrowserDevToolsButtonDebugSettings.defaultIcon.rawValue @AppStorage(BrowserDevToolsButtonDebugSettings.iconColorKey) private var devToolsIconColorRaw = BrowserDevToolsButtonDebugSettings.defaultColor.rawValue + @AppStorage(BrowserToolbarAccessorySpacingDebugSettings.key) private var browserToolbarAccessorySpacingRaw = BrowserToolbarAccessorySpacingDebugSettings.defaultSpacing + @AppStorage(BrowserProfilePopoverDebugSettings.horizontalPaddingKey) + private var browserProfilePopoverHorizontalPaddingRaw = BrowserProfilePopoverDebugSettings.defaultHorizontalPadding + @AppStorage(BrowserProfilePopoverDebugSettings.verticalPaddingKey) + private var browserProfilePopoverVerticalPaddingRaw = BrowserProfilePopoverDebugSettings.defaultVerticalPadding @AppStorage(BrowserThemeSettings.modeKey) private var browserThemeModeRaw = BrowserThemeSettings.defaultMode.rawValue @AppStorage(BrowserImportHintSettings.variantKey) private var browserImportHintVariantRaw = BrowserImportHintSettings.defaultVariant.rawValue @AppStorage(BrowserImportHintSettings.showOnBlankTabsKey) private var showBrowserImportHintOnBlankTabs = BrowserImportHintSettings.defaultShowOnBlankTabs @@ -337,6 +381,18 @@ struct BrowserPanelView: View { ) } + private var browserToolbarAccessorySpacing: CGFloat { + CGFloat(BrowserToolbarAccessorySpacingDebugSettings.resolved(browserToolbarAccessorySpacingRaw)) + } + + private var browserProfilePopoverHorizontalPadding: CGFloat { + CGFloat(BrowserProfilePopoverDebugSettings.resolvedHorizontalPadding(browserProfilePopoverHorizontalPaddingRaw)) + } + + private var browserProfilePopoverVerticalPadding: CGFloat { + CGFloat(BrowserProfilePopoverDebugSettings.resolvedVerticalPadding(browserProfilePopoverVerticalPaddingRaw)) + } + private var browserChromeBackground: Color { Color(nsColor: browserChromeStyle.backgroundColor) } @@ -475,6 +531,9 @@ struct BrowserPanelView: View { UserDefaults.standard.register(defaults: [ BrowserSearchSettings.searchEngineKey: BrowserSearchSettings.defaultSearchEngine.rawValue, BrowserSearchSettings.searchSuggestionsEnabledKey: BrowserSearchSettings.defaultSearchSuggestionsEnabled, + BrowserToolbarAccessorySpacingDebugSettings.key: BrowserToolbarAccessorySpacingDebugSettings.defaultSpacing, + BrowserProfilePopoverDebugSettings.horizontalPaddingKey: BrowserProfilePopoverDebugSettings.defaultHorizontalPadding, + BrowserProfilePopoverDebugSettings.verticalPaddingKey: BrowserProfilePopoverDebugSettings.defaultVerticalPadding, BrowserThemeSettings.modeKey: BrowserThemeSettings.defaultMode.rawValue, ]) refreshBrowserChromeStyle() @@ -487,6 +546,18 @@ struct BrowserPanelView: View { if browserImportHintVariantRaw != resolvedHintVariant.rawValue { browserImportHintVariantRaw = resolvedHintVariant.rawValue } + let resolvedToolbarAccessorySpacing = BrowserToolbarAccessorySpacingDebugSettings.resolved(browserToolbarAccessorySpacingRaw) + if browserToolbarAccessorySpacingRaw != resolvedToolbarAccessorySpacing { + browserToolbarAccessorySpacingRaw = resolvedToolbarAccessorySpacing + } + let resolvedProfilePopoverHorizontalPadding = BrowserProfilePopoverDebugSettings.resolvedHorizontalPadding(browserProfilePopoverHorizontalPaddingRaw) + if browserProfilePopoverHorizontalPaddingRaw != resolvedProfilePopoverHorizontalPadding { + browserProfilePopoverHorizontalPaddingRaw = resolvedProfilePopoverHorizontalPadding + } + let resolvedProfilePopoverVerticalPadding = BrowserProfilePopoverDebugSettings.resolvedVerticalPadding(browserProfilePopoverVerticalPaddingRaw) + if browserProfilePopoverVerticalPaddingRaw != resolvedProfilePopoverVerticalPadding { + browserProfilePopoverVerticalPaddingRaw = resolvedProfilePopoverVerticalPadding + } panel.refreshAppearanceDrivenColors() panel.setBrowserThemeMode(browserThemeMode) applyPendingAddressBarFocusRequestIfNeeded() @@ -641,12 +712,14 @@ struct BrowserPanelView: View { .accessibilityIdentifier("BrowserOmnibarPill") .accessibilityLabel("Browser omnibar") - if shouldShowToolbarImportHintChip { - browserImportHintToolbarChip + HStack(spacing: browserToolbarAccessorySpacing) { + if shouldShowToolbarImportHintChip { + browserImportHintToolbarChip + } + browserProfileButton + browserThemeModeButton + developerToolsButton } - browserProfileButton - browserThemeModeButton - developerToolsButton } .padding(.horizontal, 8) .padding(.vertical, addressBarVerticalPadding) @@ -892,7 +965,8 @@ struct BrowserPanelView: View { .buttonStyle(.plain) } } - .padding(8) + .padding(.horizontal, browserProfilePopoverHorizontalPadding) + .padding(.vertical, browserProfilePopoverVerticalPadding) .frame(minWidth: 208) } diff --git a/Sources/cmuxApp.swift b/Sources/cmuxApp.swift index 4140cbd6..343ca118 100644 --- a/Sources/cmuxApp.swift +++ b/Sources/cmuxApp.swift @@ -28,6 +28,7 @@ struct cmuxApp: App { @AppStorage(KeyboardShortcutSettings.Action.prevSidebarTab.defaultsKey) private var prevWorkspaceShortcutData = Data() @AppStorage(KeyboardShortcutSettings.Action.splitRight.defaultsKey) private var splitRightShortcutData = Data() @AppStorage(KeyboardShortcutSettings.Action.splitDown.defaultsKey) private var splitDownShortcutData = Data() + @AppStorage(BrowserToolbarAccessorySpacingDebugSettings.key) private var browserToolbarAccessorySpacingRaw = BrowserToolbarAccessorySpacingDebugSettings.defaultSpacing @AppStorage(KeyboardShortcutSettings.Action.toggleBrowserDeveloperTools.defaultsKey) private var toggleBrowserDeveloperToolsShortcutData = Data() @AppStorage(KeyboardShortcutSettings.Action.showBrowserJavaScriptConsole.defaultsKey) @@ -39,6 +40,10 @@ struct cmuxApp: App { @AppStorage(KeyboardShortcutSettings.Action.closeWorkspace.defaultsKey) private var closeWorkspaceShortcutData = Data() @NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate + private var browserToolbarAccessorySpacing: Int { + BrowserToolbarAccessorySpacingDebugSettings.resolved(browserToolbarAccessorySpacingRaw) + } + init() { if SocketControlSettings.shouldBlockUntaggedDebugLaunch() { Self.terminateForMissingLaunchTag() @@ -341,6 +346,15 @@ struct cmuxApp: App { BrowserImportHintDebugWindowController.shared.show() } + Button( + String( + localized: "debug.menu.browserProfilePopoverDebug", + defaultValue: "Browser Profile Popover Debug…" + ) + ) { + BrowserProfilePopoverDebugWindowController.shared.show() + } + Button("Settings/About Titlebar Debug…") { SettingsAboutTitlebarDebugWindowController.shared.show() } @@ -365,6 +379,29 @@ struct cmuxApp: App { } } + Menu( + String( + localized: "debug.menu.browserToolbarButtonSpacing", + defaultValue: "Browser Toolbar Button Spacing" + ) + ) { + ForEach(BrowserToolbarAccessorySpacingDebugSettings.supportedValues, id: \.self) { spacing in + Button { + browserToolbarAccessorySpacingRaw = spacing + } label: { + if browserToolbarAccessorySpacing == spacing { + Label { + Text(verbatim: "\(spacing)") + } icon: { + Image(systemName: "checkmark") + } + } else { + Text(verbatim: "\(spacing)") + } + } + } + } + Toggle("Always Show Shortcut Hints", isOn: $alwaysShowShortcutHints) Toggle( String(localized: "debug.devBuildBanner.show", defaultValue: "Show Dev Build Banner"), @@ -1065,6 +1102,7 @@ struct cmuxApp: App { private func openAllDebugWindows() { BrowserImportHintDebugWindowController.shared.show() + BrowserProfilePopoverDebugWindowController.shared.show() SettingsAboutTitlebarDebugWindowController.shared.show() SidebarDebugWindowController.shared.show() BackgroundDebugWindowController.shared.show() @@ -1698,6 +1736,14 @@ private struct DebugWindowControlsView: View { Button("Browser Import Hint Debug…") { BrowserImportHintDebugWindowController.shared.show() } + Button( + String( + localized: "debug.menu.browserProfilePopoverDebug", + defaultValue: "Browser Profile Popover Debug…" + ) + ) { + BrowserProfilePopoverDebugWindowController.shared.show() + } Button("Settings/About Titlebar Debug…") { SettingsAboutTitlebarDebugWindowController.shared.show() } @@ -1712,6 +1758,7 @@ private struct DebugWindowControlsView: View { } Button("Open All Debug Windows") { BrowserImportHintDebugWindowController.shared.show() + BrowserProfilePopoverDebugWindowController.shared.show() SettingsAboutTitlebarDebugWindowController.shared.show() SidebarDebugWindowController.shared.show() BackgroundDebugWindowController.shared.show() @@ -1949,6 +1996,205 @@ private final class BrowserImportHintDebugWindowController: NSWindowController, } } +private final class BrowserProfilePopoverDebugWindowController: NSWindowController, NSWindowDelegate { + static let shared = BrowserProfilePopoverDebugWindowController() + + private init() { + let window = NSPanel( + contentRect: NSRect(x: 0, y: 0, width: 360, height: 340), + styleMask: [.titled, .closable, .utilityWindow], + backing: .buffered, + defer: false + ) + window.title = String( + localized: "debug.windows.browserProfilePopover.title", + defaultValue: "Browser Profile Popover Debug" + ) + window.titleVisibility = .visible + window.titlebarAppearsTransparent = false + window.isMovableByWindowBackground = true + window.isReleasedWhenClosed = false + window.identifier = NSUserInterfaceItemIdentifier("cmux.browserProfilePopoverDebug") + window.center() + window.contentView = NSHostingView(rootView: BrowserProfilePopoverDebugView()) + AppDelegate.shared?.applyWindowDecorations(to: window) + super.init(window: window) + window.delegate = self + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func show() { + window?.center() + window?.makeKeyAndOrderFront(nil) + } +} + +private struct BrowserProfilePopoverDebugView: View { + @AppStorage(BrowserProfilePopoverDebugSettings.horizontalPaddingKey) + private var horizontalPaddingRaw = BrowserProfilePopoverDebugSettings.defaultHorizontalPadding + @AppStorage(BrowserProfilePopoverDebugSettings.verticalPaddingKey) + private var verticalPaddingRaw = BrowserProfilePopoverDebugSettings.defaultVerticalPadding + + private var horizontalPaddingBinding: Binding { + Binding( + get: { BrowserProfilePopoverDebugSettings.resolvedHorizontalPadding(horizontalPaddingRaw) }, + set: { horizontalPaddingRaw = BrowserProfilePopoverDebugSettings.resolvedHorizontalPadding($0) } + ) + } + + private var verticalPaddingBinding: Binding { + Binding( + get: { BrowserProfilePopoverDebugSettings.resolvedVerticalPadding(verticalPaddingRaw) }, + set: { verticalPaddingRaw = BrowserProfilePopoverDebugSettings.resolvedVerticalPadding($0) } + ) + } + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 14) { + Text( + String( + localized: "debug.browserProfilePopover.heading", + defaultValue: "Browser Profile Popover" + ) + ) + .font(.headline) + + Text( + String( + localized: "debug.browserProfilePopover.note", + defaultValue: "Tune the profile popover padding live while comparing it against the browser toolbar menu." + ) + ) + .font(.caption) + .foregroundStyle(.secondary) + + GroupBox( + String( + localized: "debug.browserProfilePopover.group.padding", + defaultValue: "Padding" + ) + ) { + VStack(alignment: .leading, spacing: 8) { + sliderRow( + String( + localized: "debug.browserProfilePopover.label.horizontal", + defaultValue: "Horizontal" + ), + value: horizontalPaddingBinding, + range: BrowserProfilePopoverDebugSettings.horizontalPaddingRange + ) + sliderRow( + String( + localized: "debug.browserProfilePopover.label.vertical", + defaultValue: "Vertical" + ), + value: verticalPaddingBinding, + range: BrowserProfilePopoverDebugSettings.verticalPaddingRange + ) + } + .padding(.top, 2) + } + + GroupBox( + String( + localized: "debug.browserProfilePopover.group.preview", + defaultValue: "Preview" + ) + ) { + profilePopoverPreview + .padding(.top, 2) + } + + HStack(spacing: 12) { + Button( + String( + localized: "debug.browserProfilePopover.reset", + defaultValue: "Reset" + ) + ) { + horizontalPaddingRaw = BrowserProfilePopoverDebugSettings.defaultHorizontalPadding + verticalPaddingRaw = BrowserProfilePopoverDebugSettings.defaultVerticalPadding + } + } + + Text( + String( + localized: "debug.browserProfilePopover.liveNote", + defaultValue: "Changes apply live to the browser profile popover." + ) + ) + .font(.caption) + .foregroundStyle(.secondary) + + Spacer(minLength: 0) + } + .padding(16) + .frame(maxWidth: .infinity, alignment: .topLeading) + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + } + + private var profilePopoverPreview: some View { + VStack(alignment: .leading, spacing: 8) { + Text(String(localized: "browser.profile.menu.title", defaultValue: "Profiles")) + .font(.system(size: 12, weight: .semibold)) + .foregroundStyle(.secondary) + + VStack(alignment: .leading, spacing: 2) { + HStack(spacing: 8) { + Image(systemName: "checkmark") + .font(.system(size: 10, weight: .semibold)) + .frame(width: 12, alignment: .center) + Text(String(localized: "browser.profile.default", defaultValue: "Default")) + .font(.system(size: 12)) + Spacer(minLength: 0) + } + .padding(.horizontal, 8) + .frame(height: 24) + .background( + RoundedRectangle(cornerRadius: 6, style: .continuous) + .fill(Color.primary.opacity(0.12)) + ) + } + + Divider() + + Text(String(localized: "browser.profile.new", defaultValue: "New Profile...")) + .font(.system(size: 12)) + + Text(String(localized: "menu.view.importFromBrowser", defaultValue: "Import From Browser…")) + .font(.system(size: 12)) + } + .padding(.horizontal, BrowserProfilePopoverDebugSettings.resolvedHorizontalPadding(horizontalPaddingRaw)) + .padding(.vertical, BrowserProfilePopoverDebugSettings.resolvedVerticalPadding(verticalPaddingRaw)) + .frame(maxWidth: .infinity, alignment: .leading) + .background( + RoundedRectangle(cornerRadius: 16, style: .continuous) + .fill(Color(nsColor: .windowBackgroundColor)) + .overlay( + RoundedRectangle(cornerRadius: 16, style: .continuous) + .stroke(Color.primary.opacity(0.08)) + ) + ) + } + + private func sliderRow(_ label: String, value: Binding, range: ClosedRange) -> some View { + HStack(spacing: 8) { + Text(label) + Slider(value: value, in: range, step: 1) + Text(String(format: "%.0f", value.wrappedValue)) + .font(.caption) + .monospacedDigit() + .frame(width: 32, alignment: .trailing) + } + } +} + private struct BrowserImportHintDebugView: View { @AppStorage(BrowserImportHintSettings.variantKey) private var variantRaw = BrowserImportHintSettings.defaultVariant.rawValue @@ -3369,6 +3615,7 @@ struct SettingsView: View { @AppStorage("sidebarTintHexLight") private var sidebarTintHexLight: String? @AppStorage("sidebarTintHexDark") private var sidebarTintHexDark: String? @AppStorage("sidebarTintOpacity") private var sidebarTintOpacity = SidebarTintDefaults.opacity + @ObservedObject private var notificationStore = TerminalNotificationStore.shared @State private var shortcutResetToken = UUID() @State private var topBlurOpacity: Double = 0 diff --git a/cmuxTests/BrowserImportMappingTests.swift b/cmuxTests/BrowserImportMappingTests.swift index e4d5f54f..58ccf28e 100644 --- a/cmuxTests/BrowserImportMappingTests.swift +++ b/cmuxTests/BrowserImportMappingTests.swift @@ -284,6 +284,39 @@ final class BrowserImportMappingTests: XCTestCase { XCTAssertTrue(lines.contains("Created cmux profiles: You, austin")) } + @MainActor + func testImportWizardCanBeConstructedForSettingsChoosePath() { + let destinationProfiles = [ + BrowserProfileDefinition( + id: UUID(uuidString: "52B43C05-4A1D-45D3-8FD5-9EF94952E445")!, + displayName: "Default", + createdAt: .distantPast, + isBuiltInDefault: true + ) + ] + let browser = makeInstalledBrowserCandidate( + descriptorID: "google-chrome", + displayName: "Chrome", + profiles: [ + makeSourceProfile(displayName: "Default", path: "/tmp/browser-import-chrome-default", isDefault: true), + makeSourceProfile(displayName: "Profile 1", path: "/tmp/browser-import-chrome-profile-1", isDefault: false), + ] + ) + + let window = BrowserDataImportCoordinator.shared.debugMakeImportWizardWindow( + browsers: [browser], + destinationProfiles: destinationProfiles, + defaultDestinationProfileID: destinationProfiles[0].id + ) + defer { + window.orderOut(nil) + window.close() + } + + XCTAssertEqual(window.title, "Import Browser Data") + XCTAssertNotNil(window.contentView) + } + private func makeSourceProfile(displayName: String, path: String, isDefault: Bool) -> InstalledBrowserProfile { InstalledBrowserProfile( displayName: displayName, @@ -291,4 +324,32 @@ final class BrowserImportMappingTests: XCTestCase { isDefault: isDefault ) } + + private func makeInstalledBrowserCandidate( + descriptorID: String, + displayName: String, + profiles: [InstalledBrowserProfile] + ) -> InstalledBrowserCandidate { + let descriptor = try! XCTUnwrap(InstalledBrowserDetector.allBrowserDescriptors.first(where: { $0.id == descriptorID })) + return InstalledBrowserCandidate( + descriptor: BrowserImportBrowserDescriptor( + id: descriptor.id, + displayName: displayName, + family: descriptor.family, + tier: descriptor.tier, + bundleIdentifiers: descriptor.bundleIdentifiers, + appNames: descriptor.appNames, + dataRootRelativePaths: descriptor.dataRootRelativePaths, + dataArtifactRelativePaths: descriptor.dataArtifactRelativePaths, + supportsDataOnlyDetection: descriptor.supportsDataOnlyDetection + ), + resolvedFamily: descriptor.family, + homeDirectoryURL: URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true), + appURL: nil, + dataRootURL: URL(fileURLWithPath: "/tmp/browser-import-\(descriptorID)", isDirectory: true), + profiles: profiles, + detectionSignals: ["test"], + detectionScore: 1 + ) + } } diff --git a/cmuxTests/CmuxWebViewKeyEquivalentTests.swift b/cmuxTests/CmuxWebViewKeyEquivalentTests.swift index 6ce551a0..a422cedd 100644 --- a/cmuxTests/CmuxWebViewKeyEquivalentTests.swift +++ b/cmuxTests/CmuxWebViewKeyEquivalentTests.swift @@ -1470,6 +1470,56 @@ final class BrowserDevToolsButtonDebugSettingsTests: XCTestCase { ) } + func testBrowserToolbarAccessorySpacingDefaultsToTwoWhenUnset() { + let defaults = makeIsolatedDefaults() + defaults.removeObject(forKey: BrowserToolbarAccessorySpacingDebugSettings.key) + + XCTAssertEqual( + BrowserToolbarAccessorySpacingDebugSettings.current(defaults: defaults), + BrowserToolbarAccessorySpacingDebugSettings.defaultSpacing + ) + } + + func testBrowserToolbarAccessorySpacingFallsBackToDefaultForUnsupportedValue() { + let defaults = makeIsolatedDefaults() + defaults.set(99, forKey: BrowserToolbarAccessorySpacingDebugSettings.key) + + XCTAssertEqual( + BrowserToolbarAccessorySpacingDebugSettings.current(defaults: defaults), + BrowserToolbarAccessorySpacingDebugSettings.defaultSpacing + ) + } + + func testBrowserProfilePopoverPaddingDefaultsWhenUnset() { + let defaults = makeIsolatedDefaults() + defaults.removeObject(forKey: BrowserProfilePopoverDebugSettings.horizontalPaddingKey) + defaults.removeObject(forKey: BrowserProfilePopoverDebugSettings.verticalPaddingKey) + + XCTAssertEqual( + BrowserProfilePopoverDebugSettings.currentHorizontalPadding(defaults: defaults), + BrowserProfilePopoverDebugSettings.defaultHorizontalPadding + ) + XCTAssertEqual( + BrowserProfilePopoverDebugSettings.currentVerticalPadding(defaults: defaults), + BrowserProfilePopoverDebugSettings.defaultVerticalPadding + ) + } + + func testBrowserProfilePopoverPaddingFallsBackForUnsupportedValues() { + let defaults = makeIsolatedDefaults() + defaults.set(-3, forKey: BrowserProfilePopoverDebugSettings.horizontalPaddingKey) + defaults.set(999, forKey: BrowserProfilePopoverDebugSettings.verticalPaddingKey) + + XCTAssertEqual( + BrowserProfilePopoverDebugSettings.currentHorizontalPadding(defaults: defaults), + BrowserProfilePopoverDebugSettings.defaultHorizontalPadding + ) + XCTAssertEqual( + BrowserProfilePopoverDebugSettings.currentVerticalPadding(defaults: defaults), + BrowserProfilePopoverDebugSettings.defaultVerticalPadding + ) + } + func testCopyPayloadUsesPersistedValues() { let defaults = makeIsolatedDefaults() defaults.set(BrowserDevToolsIconOption.scope.rawValue, forKey: BrowserDevToolsButtonDebugSettings.iconNameKey)