From ca8ca63d51e906f5f2bd2718b8027c04804184d9 Mon Sep 17 00:00:00 2001 From: idevlab Date: Sun, 9 Aug 2026 23:16:03 +0800 Subject: [PATCH 1/2] adopt macOS 26 input HUD --- Sources/UI/MenuBarView.swift | 2 +- Sources/UI/OverlayActionButton.swift | 12 +- Sources/UI/OverlayPanel.swift | 3 +- Sources/UI/OverlayPanelContent.swift | 115 +++++++++-------- Sources/UI/WaveformView.swift | 179 +++++++++++++++++++++------ 5 files changed, 206 insertions(+), 105 deletions(-) diff --git a/Sources/UI/MenuBarView.swift b/Sources/UI/MenuBarView.swift index 2291911..8f0ed71 100644 --- a/Sources/UI/MenuBarView.swift +++ b/Sources/UI/MenuBarView.swift @@ -108,7 +108,7 @@ struct MenuBarView: View { .foregroundStyle(.secondary) Spacer() WaveformView(level: appState.audioLevel) - .frame(width: 40, height: 16) + .frame(width: 58, height: 18) } if !appState.rawTranscription.isEmpty { diff --git a/Sources/UI/OverlayActionButton.swift b/Sources/UI/OverlayActionButton.swift index 67ea914..bef6fea 100644 --- a/Sources/UI/OverlayActionButton.swift +++ b/Sources/UI/OverlayActionButton.swift @@ -76,24 +76,24 @@ private struct OverlayActionButtonStyle: ButtonStyle { private var foregroundStyle: Color { switch kind { - case .cancel: return .white.opacity(0.72) - case .confirm: return .black.opacity(0.82) + case .cancel: return .primary.opacity(0.72) + case .confirm: return Color(nsColor: .alternateSelectedControlTextColor).opacity(0.92) } } private func backgroundStyle(isPressed: Bool) -> Color { switch kind { case .cancel: - return .white.opacity(isPressed ? 0.18 : isHovering ? 0.13 : 0.07) + return .primary.opacity(isPressed ? 0.18 : isHovering ? 0.13 : 0.07) case .confirm: - return .white.opacity(isPressed ? 0.76 : isHovering ? 1 : 0.9) + return Color.accentColor.opacity(isPressed ? 0.76 : isHovering ? 1 : 0.9) } } private var borderStyle: Color { switch kind { - case .cancel: return .white.opacity(0.08) - case .confirm: return .white.opacity(0.42) + case .cancel: return .primary.opacity(0.08) + case .confirm: return Color.accentColor.opacity(0.42) } } } diff --git a/Sources/UI/OverlayPanel.swift b/Sources/UI/OverlayPanel.swift index 01b01e4..c01f1de 100644 --- a/Sources/UI/OverlayPanel.swift +++ b/Sources/UI/OverlayPanel.swift @@ -67,12 +67,13 @@ final class OverlayPanel { private func apply(layout: OverlayLayout, animated: Bool) { guard let window, let hostingView else { return } let frame = frame(for: layout.panelSize, window: window) + let shouldAnimate = animated && !NSWorkspace.shared.accessibilityDisplayShouldReduceMotion hostingView.frame = NSRect(origin: .zero, size: layout.panelSize) hostingView.layer?.cornerRadius = layout.outerCornerRadius hostingView.layer?.cornerCurve = .continuous window.ignoresMouseEvents = !layout.isInteractive - window.setFrame(frame, display: true, animate: animated) + window.setFrame(frame, display: true, animate: shouldAnimate) } @MainActor diff --git a/Sources/UI/OverlayPanelContent.swift b/Sources/UI/OverlayPanelContent.swift index 6ceddaf..1e18d52 100644 --- a/Sources/UI/OverlayPanelContent.swift +++ b/Sources/UI/OverlayPanelContent.swift @@ -79,10 +79,8 @@ struct OverlayContentView: View { private var showsProgress: Bool { switch appState.phase { - case .transcribing, .processing, .inserting: - return true - default: - return false + case .transcribing, .processing, .inserting: return true + default: return false } } @@ -101,7 +99,11 @@ struct OverlayContentView: View { return false } - var body: some View { + private var panelShape: RoundedRectangle { + RoundedRectangle(cornerRadius: layout.outerCornerRadius, style: .continuous) + } + + private var panelContent: some View { VStack(spacing: layout.stackSpacing) { if layout.isInteractive { recordingControls @@ -122,37 +124,57 @@ struct OverlayContentView: View { .padding(.horizontal, layout.horizontalPadding) .padding(.bottom, layout.bottomPadding) .frame(width: layout.width, height: layout.height, alignment: .center) - .background(panelBackground) - .clipShape(RoundedRectangle(cornerRadius: layout.outerCornerRadius, style: .continuous)) - .compositingGroup() - .onAppear { - handlePhaseChange(appState.phase) - onLayoutChange(layout) - } - .onChange(of: appState.phase) { _, newPhase in - handlePhaseChange(newPhase) - } - .onChange(of: layout) { _, newLayout in - onLayoutChange(newLayout) - } - .onDisappear { - progressTimer?.invalidate() - progressTimer = nil + .containerShape(panelShape) + } + + @ViewBuilder + private var panelSurface: some View { + if reduceTransparency { + panelContent + .background(Color(nsColor: .windowBackgroundColor), in: panelShape) + .overlay { + panelShape.stroke( + Color.primary.opacity(colorSchemeContrast == .increased ? 0.36 : 0.14), + lineWidth: colorSchemeContrast == .increased ? 1.2 : 0.6 + ) + } + } else { + panelContent + .glassEffect(.regular, in: panelShape) } - .animation(reduceMotion ? nil : .easeInOut(duration: 0.18), value: layout) - .animation(reduceMotion ? nil : .easeOut(duration: 0.22), value: fakeProgress) + } + + var body: some View { + panelSurface + .onAppear { + handlePhaseChange(appState.phase) + onLayoutChange(layout) + } + .onChange(of: appState.phase) { _, newPhase in + handlePhaseChange(newPhase) + } + .onChange(of: layout) { _, newLayout in + onLayoutChange(newLayout) + } + .onDisappear { + progressTimer?.invalidate() + progressTimer = nil + } + .animation(reduceMotion ? nil : .smooth(duration: 0.18), value: layout) + .animation(reduceMotion ? nil : .smooth(duration: 0.22), value: fakeProgress) } private var statusRow: some View { HStack(spacing: 8) { statusIcon .font(.caption.weight(.medium)) + .symbolRenderingMode(.hierarchical) .frame(width: 16, height: 16) .accessibilityHidden(true) Text(appState.statusMessage) .font(.caption.weight(.medium)) - .foregroundStyle(.white.opacity(isError ? 0.94 : 0.88)) + .foregroundStyle(isError ? Color.primary : Color.secondary) .lineLimit(isError ? 2 : 1) .fixedSize(horizontal: false, vertical: true) @@ -163,15 +185,10 @@ struct OverlayContentView: View { private var recordingControls: some View { HStack(spacing: 0) { OverlayActionButton(kind: .cancel, action: onCancel) - Spacer(minLength: 0) - WaveformView(level: appState.audioLevel) - .frame(width: 42, height: 14) - .accessibilityHidden(true) - + .frame(width: 54, height: 18) Spacer(minLength: 0) - OverlayActionButton(kind: .confirm, action: onConfirm) } .frame(width: OverlayControlMetrics.recordingControlsWidth) @@ -180,7 +197,7 @@ struct OverlayContentView: View { private func livePreviewText(_ text: String) -> some View { Text(text) .font(.caption) - .foregroundStyle(.white.opacity(0.7)) + .foregroundStyle(.secondary) .lineLimit(2) .multilineTextAlignment(.leading) .frame(maxWidth: .infinity, alignment: .leading) @@ -189,11 +206,11 @@ struct OverlayContentView: View { private var progressBar: some View { GeometryReader { geo in Capsule() - .fill(.white.opacity(0.1)) + .fill(Color.primary.opacity(0.1)) .frame(height: 2) .overlay(alignment: .leading) { Capsule() - .fill(.white.opacity(0.66)) + .fill(Color.primary.opacity(0.58)) .frame(width: geo.size.width * fakeProgress, height: 2) } } @@ -202,24 +219,6 @@ struct OverlayContentView: View { .accessibilityHidden(true) } - private var panelBackground: some View { - let shape = RoundedRectangle(cornerRadius: layout.outerCornerRadius, style: .continuous) - return ZStack { - if reduceTransparency { - shape.fill(Color.black.opacity(0.92)) - } else { - shape.fill(.ultraThinMaterial) - shape.fill(Color.black.opacity(0.22)) - } - } - .overlay { - shape.strokeBorder( - .white.opacity(colorSchemeContrast == .increased ? 0.28 : 0.12), - lineWidth: colorSchemeContrast == .increased ? 1 : 0.5 - ) - } - } - private func handlePhaseChange(_ phase: AppPhase) { progressTimer?.invalidate() progressTimer = nil @@ -258,29 +257,29 @@ struct OverlayContentView: View { .foregroundStyle(Color(nsColor: .systemRed).opacity(0.82)) case .transcribing: Image(systemName: "waveform.badge.magnifyingglass") - .foregroundStyle(.white.opacity(0.76)) + .foregroundStyle(.secondary) .symbolEffect(.pulse.byLayer, isActive: !reduceMotion) case .processing: Image(systemName: "textformat") - .foregroundStyle(.white.opacity(0.76)) + .foregroundStyle(.secondary) case .inserting: Image(systemName: "text.cursor") - .foregroundStyle(.white.opacity(0.76)) + .foregroundStyle(.secondary) case .loadingModel: Image(systemName: "shippingbox.fill") - .foregroundStyle(.white.opacity(0.76)) + .foregroundStyle(.secondary) case .downloading: Image(systemName: "arrow.down") - .foregroundStyle(.white.opacity(0.76)) + .foregroundStyle(.secondary) case .done: Image(systemName: "checkmark") - .foregroundStyle(.white.opacity(0.84)) + .foregroundStyle(.primary) case .error: Image(systemName: "exclamationmark") .foregroundStyle(Color(nsColor: .systemRed).opacity(0.88)) default: Image(systemName: "mic") - .foregroundStyle(.white.opacity(0.7)) + .foregroundStyle(.secondary) } } } diff --git a/Sources/UI/WaveformView.swift b/Sources/UI/WaveformView.swift index d037fa3..a46b4a2 100644 --- a/Sources/UI/WaveformView.swift +++ b/Sources/UI/WaveformView.swift @@ -4,53 +4,154 @@ struct WaveformView: View { let level: Float @Environment(\.accessibilityReduceMotion) private var reduceMotion - - private let barCount = 7 - @State private var smoothLevel: Float = 0 + @Environment(\.colorScheme) private var colorScheme + @State private var smoothedLevel: Float = 0 var body: some View { - Group { - if reduceMotion { - waveform(at: 0) - } else { - TimelineView(.animation(minimumInterval: 1.0 / 30)) { timeline in - waveform(at: timeline.date.timeIntervalSinceReferenceDate) - } + let isDark = colorScheme == .dark + + TimelineView(.animation(minimumInterval: 1.0 / 30, paused: reduceMotion)) { timeline in + Canvas(opaque: false, rendersAsynchronously: true) { context, size in + let time = reduceMotion ? 0 : timeline.date.timeIntervalSinceReferenceDate + let energy = visibleEnergy(for: smoothedLevel) + + drawWave( + in: &context, + size: size, + time: time, + energy: energy * 0.54, + frequency: 1.55, + speed: -4.2, + phase: 1.4, + lineWidth: 1.05, + colors: isDark + ? [ + Color(red: 1.0, green: 0.34, blue: 0.27).opacity(0.32), + Color(red: 1.0, green: 0.73, blue: 0.20).opacity(0.50), + ] + : [ + Color(red: 0.78, green: 0.16, blue: 0.11).opacity(0.40), + Color(red: 0.70, green: 0.38, blue: 0.02).opacity(0.56), + ] + ) + + drawWave( + in: &context, + size: size, + time: time, + energy: energy * 0.72, + frequency: 2.15, + speed: 5.4, + phase: 2.8, + lineWidth: 1.15, + colors: isDark + ? [ + Color(red: 1.0, green: 0.48, blue: 0.28).opacity(0.48), + Color(red: 1.0, green: 0.84, blue: 0.34).opacity(0.68), + ] + : [ + Color(red: 0.84, green: 0.24, blue: 0.10).opacity(0.56), + Color(red: 0.76, green: 0.46, blue: 0.02).opacity(0.72), + ] + ) + + drawWave( + in: &context, + size: size, + time: time, + energy: energy, + frequency: 1.8, + speed: 7.2, + phase: 0, + lineWidth: 2.2, + colors: isDark + ? [ + Color(red: 1.0, green: 0.32, blue: 0.25), + Color(red: 1.0, green: 0.68, blue: 0.16), + Color(red: 1.0, green: 0.88, blue: 0.42), + ] + : [ + Color(red: 0.82, green: 0.17, blue: 0.12), + Color(red: 0.84, green: 0.39, blue: 0.02), + Color(red: 0.72, green: 0.49, blue: 0.02), + ] + ) } } - .onChange(of: level) { _, newVal in - smoothLevel = smoothLevel * 0.6 + newVal * 0.4 + .onAppear { smoothedLevel = level } + .onChange(of: level) { _, newLevel in + smoothedLevel = smoothedLevel * 0.6 + newLevel * 0.4 } + .accessibilityHidden(true) } - private func waveform(at time: TimeInterval) -> some View { - Canvas { context, size in - let barWidth: CGFloat = 1.5 - let gap: CGFloat = 2.25 - let totalWidth = CGFloat(barCount) * barWidth + CGFloat(barCount - 1) * gap - let originX = (size.width - totalWidth) / 2 - let target = max(CGFloat(level), 0.08) - let energy = target * 0.45 + CGFloat(smoothLevel) * 0.55 - - for index in 0.. CGFloat { + let normalized = CGFloat(min(max(level, 0), 1)) + return 0.14 + pow(normalized, 0.72) * 0.86 + } + + private func drawWave( + in context: inout GraphicsContext, + size: CGSize, + time: TimeInterval, + energy: CGFloat, + frequency: Double, + speed: Double, + phase: Double, + lineWidth: CGFloat, + colors: [Color] + ) { + let path = wavePath( + size: size, + time: time, + energy: energy, + frequency: frequency, + speed: speed, + phase: phase + ) + let shading = GraphicsContext.Shading.linearGradient( + Gradient(colors: colors), + startPoint: CGPoint(x: 0, y: size.height / 2), + endPoint: CGPoint(x: size.width, y: size.height / 2) + ) + context.stroke( + path, + with: shading, + style: StrokeStyle(lineWidth: lineWidth, lineCap: .round, lineJoin: .round) + ) + } + + private func wavePath( + size: CGSize, + time: TimeInterval, + energy: CGFloat, + frequency: Double, + speed: Double, + phase: Double + ) -> Path { + let sampleCount = max(Int(size.width.rounded(.up)), 32) + let centerY = size.height / 2 + let horizontalInset = min(2, size.width / 2) + let drawableWidth = max(0, size.width - horizontalInset * 2) + let maximumAmplitude = max(0, (size.height - 4) / 2) + var path = Path() + + for sample in 0...sampleCount { + let progress = Double(sample) / Double(sampleCount) + let x = horizontalInset + drawableWidth * CGFloat(progress) + let envelope = pow(sin(.pi * progress), 0.62) + let fundamental = sin(progress * .pi * 2 * frequency + time * speed + phase) + let harmonic = sin(progress * .pi * 2 * (frequency * 2.35) - time * speed * 0.44 + phase) + let signal = fundamental * 0.78 + harmonic * 0.22 + let y = centerY + maximumAmplitude * energy * CGFloat(envelope * signal) + + if sample == 0 { + path.move(to: CGPoint(x: x, y: y)) + } else { + path.addLine(to: CGPoint(x: x, y: y)) } } + + return path } } From 3d2de41269c92bb9e9138df277cb9930171d32a3 Mon Sep 17 00:00:00 2001 From: idevlab Date: Sun, 9 Aug 2026 23:28:21 +0800 Subject: [PATCH 2/2] restore original HUD capsule --- Sources/UI/MenuBarView.swift | 2 +- Sources/UI/OverlayActionButton.swift | 12 +-- Sources/UI/OverlayPanelContent.swift | 115 ++++++++++++++------------- 3 files changed, 65 insertions(+), 64 deletions(-) diff --git a/Sources/UI/MenuBarView.swift b/Sources/UI/MenuBarView.swift index 8f0ed71..2291911 100644 --- a/Sources/UI/MenuBarView.swift +++ b/Sources/UI/MenuBarView.swift @@ -108,7 +108,7 @@ struct MenuBarView: View { .foregroundStyle(.secondary) Spacer() WaveformView(level: appState.audioLevel) - .frame(width: 58, height: 18) + .frame(width: 40, height: 16) } if !appState.rawTranscription.isEmpty { diff --git a/Sources/UI/OverlayActionButton.swift b/Sources/UI/OverlayActionButton.swift index bef6fea..67ea914 100644 --- a/Sources/UI/OverlayActionButton.swift +++ b/Sources/UI/OverlayActionButton.swift @@ -76,24 +76,24 @@ private struct OverlayActionButtonStyle: ButtonStyle { private var foregroundStyle: Color { switch kind { - case .cancel: return .primary.opacity(0.72) - case .confirm: return Color(nsColor: .alternateSelectedControlTextColor).opacity(0.92) + case .cancel: return .white.opacity(0.72) + case .confirm: return .black.opacity(0.82) } } private func backgroundStyle(isPressed: Bool) -> Color { switch kind { case .cancel: - return .primary.opacity(isPressed ? 0.18 : isHovering ? 0.13 : 0.07) + return .white.opacity(isPressed ? 0.18 : isHovering ? 0.13 : 0.07) case .confirm: - return Color.accentColor.opacity(isPressed ? 0.76 : isHovering ? 1 : 0.9) + return .white.opacity(isPressed ? 0.76 : isHovering ? 1 : 0.9) } } private var borderStyle: Color { switch kind { - case .cancel: return .primary.opacity(0.08) - case .confirm: return Color.accentColor.opacity(0.42) + case .cancel: return .white.opacity(0.08) + case .confirm: return .white.opacity(0.42) } } } diff --git a/Sources/UI/OverlayPanelContent.swift b/Sources/UI/OverlayPanelContent.swift index 1e18d52..6ceddaf 100644 --- a/Sources/UI/OverlayPanelContent.swift +++ b/Sources/UI/OverlayPanelContent.swift @@ -79,8 +79,10 @@ struct OverlayContentView: View { private var showsProgress: Bool { switch appState.phase { - case .transcribing, .processing, .inserting: return true - default: return false + case .transcribing, .processing, .inserting: + return true + default: + return false } } @@ -99,11 +101,7 @@ struct OverlayContentView: View { return false } - private var panelShape: RoundedRectangle { - RoundedRectangle(cornerRadius: layout.outerCornerRadius, style: .continuous) - } - - private var panelContent: some View { + var body: some View { VStack(spacing: layout.stackSpacing) { if layout.isInteractive { recordingControls @@ -124,57 +122,37 @@ struct OverlayContentView: View { .padding(.horizontal, layout.horizontalPadding) .padding(.bottom, layout.bottomPadding) .frame(width: layout.width, height: layout.height, alignment: .center) - .containerShape(panelShape) - } - - @ViewBuilder - private var panelSurface: some View { - if reduceTransparency { - panelContent - .background(Color(nsColor: .windowBackgroundColor), in: panelShape) - .overlay { - panelShape.stroke( - Color.primary.opacity(colorSchemeContrast == .increased ? 0.36 : 0.14), - lineWidth: colorSchemeContrast == .increased ? 1.2 : 0.6 - ) - } - } else { - panelContent - .glassEffect(.regular, in: panelShape) + .background(panelBackground) + .clipShape(RoundedRectangle(cornerRadius: layout.outerCornerRadius, style: .continuous)) + .compositingGroup() + .onAppear { + handlePhaseChange(appState.phase) + onLayoutChange(layout) } - } - - var body: some View { - panelSurface - .onAppear { - handlePhaseChange(appState.phase) - onLayoutChange(layout) - } - .onChange(of: appState.phase) { _, newPhase in - handlePhaseChange(newPhase) - } - .onChange(of: layout) { _, newLayout in - onLayoutChange(newLayout) - } - .onDisappear { - progressTimer?.invalidate() - progressTimer = nil - } - .animation(reduceMotion ? nil : .smooth(duration: 0.18), value: layout) - .animation(reduceMotion ? nil : .smooth(duration: 0.22), value: fakeProgress) + .onChange(of: appState.phase) { _, newPhase in + handlePhaseChange(newPhase) + } + .onChange(of: layout) { _, newLayout in + onLayoutChange(newLayout) + } + .onDisappear { + progressTimer?.invalidate() + progressTimer = nil + } + .animation(reduceMotion ? nil : .easeInOut(duration: 0.18), value: layout) + .animation(reduceMotion ? nil : .easeOut(duration: 0.22), value: fakeProgress) } private var statusRow: some View { HStack(spacing: 8) { statusIcon .font(.caption.weight(.medium)) - .symbolRenderingMode(.hierarchical) .frame(width: 16, height: 16) .accessibilityHidden(true) Text(appState.statusMessage) .font(.caption.weight(.medium)) - .foregroundStyle(isError ? Color.primary : Color.secondary) + .foregroundStyle(.white.opacity(isError ? 0.94 : 0.88)) .lineLimit(isError ? 2 : 1) .fixedSize(horizontal: false, vertical: true) @@ -185,10 +163,15 @@ struct OverlayContentView: View { private var recordingControls: some View { HStack(spacing: 0) { OverlayActionButton(kind: .cancel, action: onCancel) + Spacer(minLength: 0) + WaveformView(level: appState.audioLevel) - .frame(width: 54, height: 18) + .frame(width: 42, height: 14) + .accessibilityHidden(true) + Spacer(minLength: 0) + OverlayActionButton(kind: .confirm, action: onConfirm) } .frame(width: OverlayControlMetrics.recordingControlsWidth) @@ -197,7 +180,7 @@ struct OverlayContentView: View { private func livePreviewText(_ text: String) -> some View { Text(text) .font(.caption) - .foregroundStyle(.secondary) + .foregroundStyle(.white.opacity(0.7)) .lineLimit(2) .multilineTextAlignment(.leading) .frame(maxWidth: .infinity, alignment: .leading) @@ -206,11 +189,11 @@ struct OverlayContentView: View { private var progressBar: some View { GeometryReader { geo in Capsule() - .fill(Color.primary.opacity(0.1)) + .fill(.white.opacity(0.1)) .frame(height: 2) .overlay(alignment: .leading) { Capsule() - .fill(Color.primary.opacity(0.58)) + .fill(.white.opacity(0.66)) .frame(width: geo.size.width * fakeProgress, height: 2) } } @@ -219,6 +202,24 @@ struct OverlayContentView: View { .accessibilityHidden(true) } + private var panelBackground: some View { + let shape = RoundedRectangle(cornerRadius: layout.outerCornerRadius, style: .continuous) + return ZStack { + if reduceTransparency { + shape.fill(Color.black.opacity(0.92)) + } else { + shape.fill(.ultraThinMaterial) + shape.fill(Color.black.opacity(0.22)) + } + } + .overlay { + shape.strokeBorder( + .white.opacity(colorSchemeContrast == .increased ? 0.28 : 0.12), + lineWidth: colorSchemeContrast == .increased ? 1 : 0.5 + ) + } + } + private func handlePhaseChange(_ phase: AppPhase) { progressTimer?.invalidate() progressTimer = nil @@ -257,29 +258,29 @@ struct OverlayContentView: View { .foregroundStyle(Color(nsColor: .systemRed).opacity(0.82)) case .transcribing: Image(systemName: "waveform.badge.magnifyingglass") - .foregroundStyle(.secondary) + .foregroundStyle(.white.opacity(0.76)) .symbolEffect(.pulse.byLayer, isActive: !reduceMotion) case .processing: Image(systemName: "textformat") - .foregroundStyle(.secondary) + .foregroundStyle(.white.opacity(0.76)) case .inserting: Image(systemName: "text.cursor") - .foregroundStyle(.secondary) + .foregroundStyle(.white.opacity(0.76)) case .loadingModel: Image(systemName: "shippingbox.fill") - .foregroundStyle(.secondary) + .foregroundStyle(.white.opacity(0.76)) case .downloading: Image(systemName: "arrow.down") - .foregroundStyle(.secondary) + .foregroundStyle(.white.opacity(0.76)) case .done: Image(systemName: "checkmark") - .foregroundStyle(.primary) + .foregroundStyle(.white.opacity(0.84)) case .error: Image(systemName: "exclamationmark") .foregroundStyle(Color(nsColor: .systemRed).opacity(0.88)) default: Image(systemName: "mic") - .foregroundStyle(.secondary) + .foregroundStyle(.white.opacity(0.7)) } } }