// MacDictationOverlayView.swift // OSGKeyboard · Mac // // Compact bottom-of-screen HUD shown while dictating. Lives inside a // non-activating NSPanel so it never steals focus from the front app. // One-line layout: status / live transcript preview + waveform + stop. import SwiftUI struct MacDictationOverlayView: View { @ObservedObject var viewModel: MacDictationViewModel @Environment(\.themePalette) private var palette private var lang: AppUILanguage { viewModel.config.uiLanguage } private var isBusy: Bool { viewModel.isRecording || viewModel.isPreparingToRecord || viewModel.isProcessing } /// Trimmed live / final transcript for the single-line preview. private var previewText: String { viewModel.transcript.trimmingCharacters(in: .whitespacesAndNewlines) } private var hasPreview: Bool { !previewText.isEmpty } private var showsLiveBadge: Bool { viewModel.isRecording && viewModel.isStreamingPartial } var body: some View { HStack(spacing: Spacing.sm) { statusDot primaryLine Spacer(minLength: Spacing.xs) trailingControl } .padding(.horizontal, Spacing.md) .padding(.vertical, 11) .frame(minWidth: 300, idealWidth: 400, maxWidth: 520) .fixedSize(horizontal: true, vertical: true) .background(palette.surface, in: Capsule(style: .continuous)) .overlay( Capsule(style: .continuous) .stroke(palette.dividerStrong, lineWidth: 0.5) ) .shadow(color: Color.black.opacity(0.22), radius: 14, y: 5) .padding(2) .animation(Motion.soft, value: hasPreview) .animation(Motion.quick, value: viewModel.isRecording) .animation(Motion.quick, value: viewModel.isStreamingPartial) } // MARK: - Primary line (status or one-line transcript) @ViewBuilder private var primaryLine: some View { if hasPreview { HStack(spacing: 6) { if showsLiveBadge { liveBadge } Text(previewText) .font(TypeStyle.footnote) .foregroundStyle(palette.textPrimary) .lineLimit(1) .truncationMode(.head) .frame(maxWidth: showsLiveBadge ? 280 : 320, alignment: .leading) .contentTransition(.opacity) .animation(Motion.quick, value: previewText) .accessibilityLabel(previewText) } } else { HStack(spacing: 6) { Text(statusText) .font(TypeStyle.caption) .foregroundStyle(palette.textPrimary) .contentTransition(.opacity) if let appName = viewModel.foregroundAppName, isBusy { Text("·") .foregroundStyle(palette.textTertiary.opacity(0.45)) Text(appName) .font(TypeStyle.caption) .foregroundStyle(palette.textTertiary) .lineLimit(1) } } .animation(Motion.quick, value: statusText) } } private var liveBadge: some View { Text(MacL10n.string("mac.overlay.live", language: lang)) .font(.system(size: 9, weight: .semibold)) .foregroundStyle(palette.recordRed) .padding(.horizontal, 5) .padding(.vertical, 2) .background(palette.recordRed.opacity(0.12), in: Capsule(style: .continuous)) .accessibilityHidden(true) } private var statusDot: some View { Circle() .fill(dotColor) .frame(width: 8, height: 8) .animation(Motion.quick, value: viewModel.isRecording) .animation(Motion.quick, value: viewModel.isProcessing) .animation(Motion.quick, value: viewModel.isPreparingToRecord) .animation(Motion.quick, value: viewModel.isStreamingPartial) } private var dotColor: Color { if viewModel.isRecording { return viewModel.isStreamingPartial ? palette.accent : palette.recordRed } if viewModel.isPreparingToRecord || viewModel.isProcessing { return palette.warning } return palette.accent } private var statusText: String { if viewModel.isRecording { return MacL10n.string("mac.overlay.listening", language: lang) } if viewModel.isPreparingToRecord { return MacL10n.string("mac.overlay.preparing", language: lang) } if viewModel.isProcessing { if viewModel.isStreamingPartial { return MacL10n.string("mac.overlay.polishing", language: lang) } return MacL10n.string("mac.overlay.transcribing", language: lang) } return MacL10n.string("mac.overlay.done", language: lang) } @ViewBuilder private var trailingControl: some View { if viewModel.isRecording { MiniWaveform( level: viewModel.audioLevel, barCount: 7, tint: (viewModel.isStreamingPartial ? palette.accent : palette.recordRed) .opacity(0.9), maxBarHeight: 28, barWidth: 3.5, barSpacing: 2.5 ) stopButton } else if viewModel.isPreparingToRecord || viewModel.isProcessing { ProgressView() .controlSize(.small) } else { Image(systemName: "checkmark.circle.fill") .foregroundStyle(palette.accent) .symbolRenderingMode(.hierarchical) } } private var stopButton: some View { Button(action: viewModel.toggleRecording) { Image(systemName: "stop.fill") .font(.system(size: 10, weight: .bold)) .foregroundStyle(palette.textOnAccent) .frame(width: 28, height: 28) .background(palette.recordRed, in: Circle()) } .buttonStyle(.plain) .accessibilityLabel(MacL10n.string("mac.record.stop", language: lang)) } }