From 265b4c9f13d7177f2d4e17b8a8353ef729e97225 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 3 Jul 2026 08:30:20 +0000 Subject: [PATCH] feat(ui): simplify keyboard chips and reorganize settings preferences - Remove polish scenario and ASR locale chips from keyboard top bar - Fix polish intensity labels via SharedL10n (Shared.strings table) - Merge polish intensity and personal dictionary into preferences card - Personal dictionary: no list icon; detail view uses nav bar + back Co-authored-by: Rocky --- .../Views/PersonalDictionaryView.swift | 68 ++++++----- OSGKeyboard/Views/SettingsView.swift | 111 ++++++++---------- OSGKeyboardExt/Views/KeyboardRootView.swift | 13 +- 3 files changed, 91 insertions(+), 101 deletions(-) diff --git a/OSGKeyboard/Views/PersonalDictionaryView.swift b/OSGKeyboard/Views/PersonalDictionaryView.swift index 50f5e30..91a142f 100644 --- a/OSGKeyboard/Views/PersonalDictionaryView.swift +++ b/OSGKeyboard/Views/PersonalDictionaryView.swift @@ -24,41 +24,51 @@ import OSGKeyboardShared @MainActor struct PersonalDictionaryView: View { @Environment(\.themePalette) private var palette: ThemePalette - @Environment(\.dismiss) private var dismiss + @ObservedObject private var config = ProviderConfig.shared @State private var dictionary: PersonalDictionary = AppGroupStore().personalDictionary @State private var searchText: String = "" + @State private var showClearAllConfirmation = false private let store = AppGroupStore() var body: some View { - NavigationStack { - VStack(spacing: 0) { - PageHeaderRow(title: "settings.personalDictionary.title") { - if !dictionary.entries.isEmpty { - PageHeaderConfirmButton( - systemImage: "trash", - accessibilityLabel: "settings.personalDictionary.clearAll", - confirmTitle: "settings.personalDictionary.clearAll.confirmTitle", - confirmMessage: "settings.personalDictionary.clearAll.message", - confirmActionTitle: "settings.personalDictionary.clearAll.confirm" - ) { - clearAll() - } - } - } - - ZStack { - palette.background.ignoresSafeArea() - if dictionary.entries.isEmpty { - emptyState - } else { - list + Group { + if dictionary.entries.isEmpty { + emptyState + } else { + list + } + } + .background(palette.background.ignoresSafeArea()) + .navigationTitle("settings.personalDictionary.title") + .navigationBarTitleDisplayMode(.inline) + .toolbar(.visible, for: .navigationBar) + .toolbar { + if !dictionary.entries.isEmpty { + ToolbarItem(placement: .topBarTrailing) { + Button { + showClearAllConfirmation = true + } label: { + Image(systemName: "trash") } + .accessibilityLabel(AppL10n.string("settings.personalDictionary.clearAll")) } } - .background(palette.background) - .toolbar(.hidden, for: .navigationBar) + } + .toolbarBackground(palette.background, for: .navigationBar) + .toolbarBackground(.visible, for: .navigationBar) + .confirmationDialog( + AppL10n.string("settings.personalDictionary.clearAll.confirmTitle"), + isPresented: $showClearAllConfirmation, + titleVisibility: .visible + ) { + Button(AppL10n.string("settings.personalDictionary.clearAll.confirm"), role: .destructive) { + clearAll() + } + Button(AppL10n.string("common.cancel"), role: .cancel) {} + } message: { + Text("settings.personalDictionary.clearAll.message") } } @@ -103,7 +113,7 @@ struct PersonalDictionaryView: View { private func section(for category: PersonalDictionary.Entry.Category, items: [PersonalDictionary.Entry]) -> some View { VStack(alignment: .leading, spacing: Spacing.sm) { - Text(LocalizedStringKey(category.labelKey)) + Text(SharedL10n.string(category.labelKey, language: config.uiLanguage)) .font(TypeStyle.caption2) .foregroundStyle(palette.textSecondary) .textCase(.uppercase) @@ -131,7 +141,7 @@ struct PersonalDictionaryView: View { .foregroundStyle(palette.textPrimary) .lineLimit(1) HStack(spacing: 6) { - Text(LocalizedStringKey(entry.source.labelKey)) + Text(SharedL10n.string(entry.source.labelKey, language: config.uiLanguage)) .font(TypeStyle.caption2) .foregroundStyle(palette.textTertiary) if entry.usageCount > 1 { @@ -231,7 +241,9 @@ struct PersonalDictionaryView: View { #if DEBUG #Preview { ThemedRoot { - PersonalDictionaryView() + NavigationStack { + PersonalDictionaryView() + } } } #endif diff --git a/OSGKeyboard/Views/SettingsView.swift b/OSGKeyboard/Views/SettingsView.swift index 6eaf8e9..2c98f76 100644 --- a/OSGKeyboard/Views/SettingsView.swift +++ b/OSGKeyboard/Views/SettingsView.swift @@ -74,11 +74,9 @@ struct SettingsView: View { providerSection apiSection } - polishIntensitySection if config.engineMode == "local" { localEngineSettingsSection } - personalDictionaryLinkSection if presentation == .tab { preferencesSection footerLinks @@ -268,6 +266,19 @@ struct SettingsView: View { set: { config.handednessPreference = $0 } ) ) + + Divider().background(palette.divider) + + polishIntensityPreferenceRows + + Divider().background(palette.divider) + + NavigationLink { + PersonalDictionaryView() + } label: { + personalDictionaryPreferenceRow + } + .buttonStyle(.plain) } .background(palette.surface, in: RoundedRectangle(cornerRadius: Radius.xl, style: .continuous)) .overlay( @@ -277,74 +288,50 @@ struct SettingsView: View { } } - // MARK: - Polish intensity (v0.3.0) - - private var polishIntensitySection: some View { - VStack(alignment: .leading, spacing: SettingsListMetrics.sectionLabelSpacing) { - sectionHeader("settings.polishIntensity.title") - VStack(spacing: 0) { - Picker("", selection: $config.polishIntensity) { - ForEach(PolishIntensity.allCases, id: \.self) { intensity in - Text(LocalizedStringKey(intensity.labelKey)) - .tag(intensity) - } - } - .pickerStyle(.segmented) + private var polishIntensityPreferenceRows: some View { + VStack(alignment: .leading, spacing: Spacing.sm) { + Text("settings.polishIntensity.title") + .font(TypeStyle.body) + .foregroundStyle(palette.textPrimary) .padding(.horizontal, Spacing.md) - .padding(.vertical, Spacing.sm) + .padding(.top, Spacing.sm) - Text(LocalizedStringKey(config.polishIntensity.descriptionKey)) - .font(TypeStyle.caption2) - .foregroundStyle(palette.textSecondary) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.horizontal, Spacing.md) - .padding(.bottom, Spacing.sm) + Picker("", selection: $config.polishIntensity) { + ForEach(PolishIntensity.allCases, id: \.self) { intensity in + Text(SharedL10n.string(intensity.labelKey, language: config.uiLanguage)) + .tag(intensity) + } } - .background(palette.surface, in: RoundedRectangle(cornerRadius: Radius.large, style: .continuous)) - .overlay( - RoundedRectangle(cornerRadius: Radius.large, style: .continuous) - .stroke(palette.divider, lineWidth: 0.5) - ) + .pickerStyle(.segmented) + .padding(.horizontal, Spacing.md) + + Text(SharedL10n.string(config.polishIntensity.descriptionKey, language: config.uiLanguage)) + .font(TypeStyle.caption2) + .foregroundStyle(palette.textSecondary) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, Spacing.md) + .padding(.bottom, Spacing.sm) } } - // MARK: - Personal dictionary (v0.3.0) - - private var personalDictionaryLinkSection: some View { - VStack(alignment: .leading, spacing: SettingsListMetrics.sectionLabelSpacing) { - sectionHeader("settings.personalDictionary.sectionTitle") - VStack(spacing: 0) { - NavigationLink { - PersonalDictionaryView() - } label: { - HStack(spacing: Spacing.sm) { - MaterialIcon(name: .bookmark, size: 18) - .foregroundStyle(palette.accent) - VStack(alignment: .leading, spacing: 2) { - Text("settings.personalDictionary.title") - .font(TypeStyle.body) - .foregroundStyle(palette.textPrimary) - Text("settings.personalDictionary.summary") - .font(TypeStyle.caption2) - .foregroundStyle(palette.textSecondary) - } - Spacer() - Image(systemName: "chevron.right") - .font(.system(size: 14, weight: .semibold)) - .foregroundStyle(palette.textTertiary) - } - .padding(.horizontal, Spacing.md) - .frame(minHeight: SettingsListMetrics.singleLineMinHeight) - .contentShape(Rectangle()) - } - .buttonStyle(.plain) + private var personalDictionaryPreferenceRow: some View { + HStack(spacing: Spacing.sm) { + VStack(alignment: .leading, spacing: 2) { + Text("settings.personalDictionary.title") + .font(TypeStyle.body) + .foregroundStyle(palette.textPrimary) + Text("settings.personalDictionary.summary") + .font(TypeStyle.caption2) + .foregroundStyle(palette.textSecondary) } - .background(palette.surface, in: RoundedRectangle(cornerRadius: Radius.large, style: .continuous)) - .overlay( - RoundedRectangle(cornerRadius: Radius.large, style: .continuous) - .stroke(palette.divider, lineWidth: 0.5) - ) + Spacer() + Image(systemName: "chevron.right") + .font(.system(size: 14, weight: .semibold)) + .foregroundStyle(palette.textTertiary) } + .padding(.horizontal, Spacing.md) + .frame(minHeight: SettingsListMetrics.doubleLineMinHeight) + .contentShape(Rectangle()) } // MARK: - Footer links (tab settings only) diff --git a/OSGKeyboardExt/Views/KeyboardRootView.swift b/OSGKeyboardExt/Views/KeyboardRootView.swift index c0563af..72dad05 100644 --- a/OSGKeyboardExt/Views/KeyboardRootView.swift +++ b/OSGKeyboardExt/Views/KeyboardRootView.swift @@ -140,17 +140,8 @@ public struct KeyboardRootView: View { } else { CloudEngineChip() } - if state.isPolishScenarioChipVisible { - ScenarioChip(state: state) - } - LocaleChip(localeId: state.localeId) { newId in - state.setLocale(newId) - } - // v0.3.0: detected app context — the per-app polish mode. - // The chip mirrors `AppGroupStore.detectedAppContext` and - // writes overrides back so the next LLM call uses the new - // tone. Hidden during onboarding (the overlay reads better - // without chip clutter). + // Polish scenario and ASR locale are configured in the main-app + // Settings tab only — keep the keyboard top bar uncluttered. if state.hasCompletedOnboarding { AppContextChip(state: state) }