From be91184ad5fb928ce180926daca53d9ff9ce36cf Mon Sep 17 00:00:00 2001 From: geekrebel Date: Mon, 29 Jun 2026 09:58:04 +1000 Subject: [PATCH] =?UTF-8?q?Onboarding:=20make=20model-card=20info=20(?= =?UTF-8?q?=E2=93=98)=20icon=20a=20real=20button=20with=20click-to-open=20?= =?UTF-8?q?popover?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The info icon on each voice-engine card in onboarding was a static Image whose details were only reachable via the hover help tag. The glyph reads as an interactive control but had no click action and was not keyboard/VoiceOver focusable, so the information was effectively hover-only. Wrap it in a Button that toggles a popover (reusing the existing info-popover pattern from CustomDictionaryView), keep the help tag as a supplement, and give it a proper accessibility label and hint. Co-Authored-By: Claude Opus 4.8 --- Sources/Fluid/UI/WelcomeView.swift | 49 +++++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/Sources/Fluid/UI/WelcomeView.swift b/Sources/Fluid/UI/WelcomeView.swift index 1ed5edd2..afb965e0 100644 --- a/Sources/Fluid/UI/WelcomeView.swift +++ b/Sources/Fluid/UI/WelcomeView.swift @@ -598,6 +598,7 @@ struct OnboardingFlowView: View { @State private var selectedModelRouteID: String? @State private var hoveredLanguageID: String? @State private var hoveredModelRouteID: String? + @State private var infoPopoverRouteID: String? @State private var hoveredModelActionButtonID: String? @State private var hoveredPermissionButtonID: String? @State private var hoveredFooterButton: OnboardingFooterButton? @@ -2002,13 +2003,28 @@ struct OnboardingFlowView: View { Spacer(minLength: 8) - Image(systemName: "info.circle") - .font(self.theme.typography.sectionTitle) - .foregroundStyle(Color.white.opacity(0.58)) - .frame(width: 24, height: 24) - .contentShape(Circle()) - .help(self.onboardingModelTooltip(for: route)) - .accessibilityLabel(self.onboardingModelTooltip(for: route)) + Button { + self.infoPopoverRouteID = (self.infoPopoverRouteID == route.id) ? nil : route.id + } label: { + Image(systemName: "info.circle") + .font(self.theme.typography.sectionTitle) + .foregroundStyle(Color.white.opacity(0.58)) + .frame(width: 28, height: 28) + .contentShape(Circle()) + } + .buttonStyle(.plain) + .help(self.onboardingModelTooltip(for: route)) + .accessibilityLabel("About \(self.onboardingModelTitle(for: model))") + .accessibilityHint("Shows details for this voice model") + .popover( + isPresented: Binding( + get: { self.infoPopoverRouteID == route.id }, + set: { isShown in self.infoPopoverRouteID = isShown ? route.id : nil } + ), + arrowEdge: .top + ) { + self.onboardingModelInfoPopover(for: route) + } } .frame(height: 38, alignment: .top) @@ -2388,6 +2404,25 @@ struct OnboardingFlowView: View { return "\(self.onboardingModelSubtitle(for: model)) - \(model.downloadSize)\n\(model.cardDescription)" } + private func onboardingModelInfoPopover(for route: VoiceEngineLanguageRoute) -> some View { + let model = route.model + return VStack(alignment: .leading, spacing: 8) { + Text(self.onboardingModelTitle(for: model)) + .font(self.theme.typography.bodySmallStrong) + + Text("\(self.onboardingModelSubtitle(for: model)) ยท \(model.downloadSize)") + .font(self.theme.typography.captionStrong) + .foregroundStyle(self.theme.palette.accent) + + Text(model.cardDescription) + .font(self.theme.typography.caption) + .foregroundStyle(self.theme.palette.secondaryText) + .fixedSize(horizontal: false, vertical: true) + } + .padding(16) + .frame(width: 300, alignment: .leading) + } + private func onboardingModelTitle(for model: SettingsStore.SpeechModel) -> String { model.humanReadableName }