Add browser import hint debug variants

This commit is contained in:
Lawrence Chen 2026-03-17 03:01:50 -07:00
parent f97716939a
commit b9de0f0446
No known key found for this signature in database
7 changed files with 892 additions and 22 deletions

View file

@ -250,6 +250,9 @@ struct BrowserPanelView: View {
@AppStorage(BrowserDevToolsButtonDebugSettings.iconNameKey) private var devToolsIconNameRaw = BrowserDevToolsButtonDebugSettings.defaultIcon.rawValue
@AppStorage(BrowserDevToolsButtonDebugSettings.iconColorKey) private var devToolsIconColorRaw = BrowserDevToolsButtonDebugSettings.defaultColor.rawValue
@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
@AppStorage(BrowserImportHintSettings.dismissedKey) private var isBrowserImportHintDismissed = BrowserImportHintSettings.defaultDismissed
@AppStorage(KeyboardShortcutSettings.Action.toggleBrowserDeveloperTools.defaultsKey)
private var toggleBrowserDeveloperToolsShortcutData = Data()
@State private var suggestionTask: Task<Void, Never>?
@ -267,6 +270,7 @@ struct BrowserPanelView: View {
@State private var focusFlashAnimationGeneration: Int = 0
@State private var omnibarPillFrame: CGRect = .zero
@State private var addressBarHeight: CGFloat = 0
@State private var isBrowserImportHintPopoverPresented = false
@State private var lastHandledAddressBarFocusRequestId: UUID?
@State private var pendingAddressBarFocusRetryRequestId: UUID?
@State private var pendingAddressBarFocusRetryGeneration: UInt64 = 0
@ -321,6 +325,18 @@ struct BrowserPanelView: View {
BrowserThemeSettings.mode(for: browserThemeModeRaw)
}
private var browserImportHintVariant: BrowserImportHintVariant {
BrowserImportHintSettings.variant(for: browserImportHintVariantRaw)
}
private var browserImportHintPresentation: BrowserImportHintPresentation {
BrowserImportHintPresentation(
variant: browserImportHintVariant,
showOnBlankTabs: showBrowserImportHintOnBlankTabs,
isDismissed: isBrowserImportHintDismissed
)
}
private var browserChromeBackground: Color {
Color(nsColor: browserChromeStyle.backgroundColor)
}
@ -346,6 +362,14 @@ struct BrowserPanelView: View {
return "\(base) (\(toggleBrowserDeveloperToolsShortcut.displayString))"
}
private var browserImportHintSummary: String {
InstalledBrowserDetector.summaryText(for: emptyStateImportBrowsers)
}
private var shouldShowToolbarImportHintChip: Bool {
shouldShowEmptyStateImportOverlay && browserImportHintPresentation.blankTabPlacement == .toolbarChip
}
private var owningWorkspace: Workspace? {
guard let app = AppDelegate.shared,
let manager = app.tabManagerFor(tabId: panel.workspaceId) else {
@ -459,6 +483,10 @@ struct BrowserPanelView: View {
if browserThemeModeRaw != resolvedThemeMode.rawValue {
browserThemeModeRaw = resolvedThemeMode.rawValue
}
let resolvedHintVariant = BrowserImportHintSettings.variant(for: browserImportHintVariantRaw)
if browserImportHintVariantRaw != resolvedHintVariant.rawValue {
browserImportHintVariantRaw = resolvedHintVariant.rawValue
}
panel.refreshAppearanceDrivenColors()
panel.setBrowserThemeMode(browserThemeMode)
applyPendingAddressBarFocusRequestIfNeeded()
@ -613,6 +641,9 @@ struct BrowserPanelView: View {
.accessibilityIdentifier("BrowserOmnibarPill")
.accessibilityLabel("Browser omnibar")
if shouldShowToolbarImportHintChip {
browserImportHintToolbarChip
}
browserProfileButton
browserThemeModeButton
developerToolsButton
@ -776,6 +807,29 @@ struct BrowserPanelView: View {
.accessibilityIdentifier("BrowserThemeModeButton")
}
private var browserImportHintToolbarChip: some View {
Button(action: {
isBrowserImportHintPopoverPresented.toggle()
}) {
HStack(spacing: 4) {
Image(systemName: "square.and.arrow.down.on.square")
.font(.system(size: 10, weight: .medium))
Text(String(localized: "browser.import.hint.toolbar", defaultValue: "Import"))
.font(.system(size: 11, weight: .medium))
.lineLimit(1)
}
.foregroundStyle(devToolsColorOption.color)
.padding(.horizontal, 8)
.padding(.vertical, 4)
}
.buttonStyle(OmnibarAddressButtonStyle())
.popover(isPresented: $isBrowserImportHintPopoverPresented, arrowEdge: .bottom) {
browserImportHintPopover
}
.safeHelp(String(localized: "browser.import.hint.toolbar.help", defaultValue: "Import browser data"))
.accessibilityIdentifier("BrowserImportHintToolbarChip")
}
private var browserProfilePopover: some View {
VStack(alignment: .leading, spacing: 8) {
Text(String(localized: "browser.profile.menu.title", defaultValue: "Profiles"))
@ -1018,9 +1072,16 @@ struct BrowserPanelView: View {
setAddressBarFocused(false, reason: "placeholderContent.tapBlur")
}
}
.overlay(alignment: .topLeading) {
if shouldShowEmptyStateImportOverlay,
browserImportHintPresentation.blankTabPlacement == .inlineStrip {
emptyBrowserStateInlineStrip
}
}
.overlay {
if shouldShowEmptyStateImportOverlay {
emptyBrowserStateOverlay
if shouldShowEmptyStateImportOverlay,
browserImportHintPresentation.blankTabPlacement == .floatingCard {
emptyBrowserStateCardOverlay
}
}
}
@ -1288,28 +1349,11 @@ struct BrowserPanelView: View {
#endif
}
private var emptyBrowserStateOverlay: some View {
private var emptyBrowserStateCardOverlay: some View {
VStack {
Spacer(minLength: 22)
VStack(alignment: .leading, spacing: 8) {
Text(String(localized: "settings.browser.emptyImport.title", defaultValue: "Import browser data"))
.font(.system(size: 13, weight: .medium))
.foregroundStyle(.secondary)
Text(InstalledBrowserDetector.summaryText(for: emptyStateImportBrowsers))
.font(.system(size: 12))
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
Button(String(localized: "settings.browser.emptyImport.choose", defaultValue: "Choose What to Import…")) {
BrowserDataImportCoordinator.shared.presentImportDialog(
defaultDestinationProfileID: panel.profileID
)
}
.buttonStyle(.bordered)
.controlSize(.small)
}
browserImportHintBody
.padding(12)
.frame(maxWidth: 360, alignment: .leading)
.background(
@ -1329,10 +1373,118 @@ struct BrowserPanelView: View {
.padding(.horizontal, 18)
}
private var emptyBrowserStateInlineStrip: some View {
VStack(alignment: .leading, spacing: 0) {
browserImportHintBody
.padding(.horizontal, 12)
.padding(.vertical, 10)
.frame(maxWidth: 520, alignment: .leading)
.background(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.fill(Color(nsColor: .windowBackgroundColor).opacity(0.84))
)
.overlay(
RoundedRectangle(cornerRadius: 12, style: .continuous).stroke(
Color(nsColor: .separatorColor).opacity(0.35),
lineWidth: 1
)
)
.shadow(color: Color.black.opacity(0.05), radius: 6, y: 2)
Spacer(minLength: 0)
}
.padding(.horizontal, 18)
.padding(.top, 14)
}
private var browserImportHintPopover: some View {
browserImportHintBody
.padding(12)
.frame(width: 300, alignment: .leading)
}
private var browserImportHintBody: some View {
VStack(alignment: .leading, spacing: 8) {
Text(String(localized: "browser.import.hint.title", defaultValue: "Import browser data"))
.font(.system(size: 12.5, weight: .semibold))
Text(browserImportHintSummary)
.font(.system(size: 11.5))
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
Text(String(localized: "browser.import.hint.settingsFootnote", defaultValue: "You can always find this in Settings > Browser."))
.font(.system(size: 10.5))
.foregroundStyle(.tertiary)
.fixedSize(horizontal: false, vertical: true)
ViewThatFits(in: .horizontal) {
HStack(spacing: 10) {
browserImportHintPrimaryButton
browserImportHintSettingsButton
browserImportHintDismissButton
}
VStack(alignment: .leading, spacing: 8) {
browserImportHintPrimaryButton
HStack(spacing: 10) {
browserImportHintSettingsButton
browserImportHintDismissButton
}
}
}
}
.accessibilityElement(children: .contain)
}
private var browserImportHintPrimaryButton: some View {
Button(String(localized: "browser.import.hint.import", defaultValue: "Import…")) {
presentImportDialogFromHint()
}
.buttonStyle(.bordered)
.controlSize(.small)
}
private var browserImportHintSettingsButton: some View {
Button(String(localized: "browser.import.hint.settings", defaultValue: "Browser Settings")) {
openBrowserImportSettings()
}
.buttonStyle(.plain)
.controlSize(.small)
.accessibilityIdentifier("BrowserImportHintSettingsButton")
}
private var browserImportHintDismissButton: some View {
Button(String(localized: "browser.import.hint.dismiss", defaultValue: "Hide Hint")) {
dismissBrowserImportHint()
}
.buttonStyle(.plain)
.controlSize(.small)
.accessibilityIdentifier("BrowserImportHintDismissButton")
}
private var shouldShowEmptyStateImportOverlay: Bool {
!panel.shouldRenderWebView && isWebViewBlank()
}
private func presentImportDialogFromHint() {
isBrowserImportHintPopoverPresented = false
BrowserDataImportCoordinator.shared.presentImportDialog(
defaultDestinationProfileID: panel.profileID
)
}
private func openBrowserImportSettings() {
isBrowserImportHintPopoverPresented = false
AppDelegate.presentPreferencesWindow(navigationTarget: .browser)
}
private func dismissBrowserImportHint() {
showBrowserImportHintOnBlankTabs = false
isBrowserImportHintDismissed = true
isBrowserImportHintPopoverPresented = false
}
/// Treat a WebView with no URL (or about:blank) as "blank" for UX purposes.
private func isWebViewBlank() -> Bool {
guard let url = panel.webView.url else { return true }