Merge pull request #16 from hkgood/cursor/settings-keyboard-ui-tweaks-6baf
feat(ui): keyboard chip cleanup and settings preferences merge
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user