feat(keyboard): ship AI hint carousel, home library cards, and clipboard polish
Rotate AI idle suggestions with optional remote packs, move history/dictionary onto self-sizing Home preview cards, harden clipboard capture/prompting, and simplify keyboard chrome by dropping most liquid-glass shadows.
This commit is contained in:
@@ -32,7 +32,11 @@ public struct EditTextPager: View {
|
||||
public var body: some View {
|
||||
GeometryReader { proxy in
|
||||
ScrollView(.horizontal) {
|
||||
LazyHStack(spacing: 0) {
|
||||
// Two pages only — prefer HStack so page `1` is laid out in the
|
||||
// same update that sets `scrollPosition`, avoiding a stuck
|
||||
// LazyHStack that leaves the indicator on “edited” while still
|
||||
// showing the original text.
|
||||
HStack(spacing: 0) {
|
||||
textPage(title: originalTitle, text: originalText)
|
||||
.frame(
|
||||
width: proxy.size.width,
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
// PolishStyleIconBadge.swift
|
||||
// OSGKeyboard · Shared
|
||||
//
|
||||
// Circular SF Symbol badge for polish-style cards. Fixed footprint keeps icons
|
||||
// visually consistent across built-in and user-defined styles on iOS and macOS.
|
||||
|
||||
import SwiftUI
|
||||
|
||||
public struct PolishStyleIconBadge: View {
|
||||
@Environment(\.themePalette) private var palette
|
||||
|
||||
public let systemImage: String
|
||||
public var isSelected: Bool
|
||||
|
||||
private let circleSize: CGFloat = 40
|
||||
private let iconSize: CGFloat = 18
|
||||
|
||||
public init(pack: PolishStylePack, isSelected: Bool = false) {
|
||||
self.systemImage = PolishStylePackCatalog.systemImage(for: pack.id)
|
||||
self.isSelected = isSelected
|
||||
}
|
||||
|
||||
public init(systemImage: String, isSelected: Bool = false) {
|
||||
self.systemImage = systemImage
|
||||
self.isSelected = isSelected
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(isSelected ? palette.accentMuted : palette.surfaceMuted)
|
||||
.frame(width: circleSize, height: circleSize)
|
||||
Image(systemName: systemImage)
|
||||
.font(.system(size: iconSize, weight: .medium))
|
||||
.foregroundStyle(isSelected ? palette.accent : palette.textSecondary)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
}
|
||||
.frame(width: circleSize, height: circleSize)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ public struct RecordButton: View {
|
||||
public let level: Double
|
||||
public let remainingSeconds: Int?
|
||||
public let isEnabled: Bool
|
||||
public let usesLiquidGlass: Bool
|
||||
public let onToggle: () -> Void
|
||||
public let onPressingChanged: (Bool) -> Void
|
||||
/// When non-nil, a 0.45s hold starts explicit editing of the last insertion.
|
||||
@@ -42,6 +43,7 @@ public struct RecordButton: View {
|
||||
level: Double,
|
||||
remainingSeconds: Int? = nil,
|
||||
isEnabled: Bool = true,
|
||||
usesLiquidGlass: Bool = false,
|
||||
onToggle: @escaping () -> Void,
|
||||
onPressingChanged: @escaping (Bool) -> Void = { _ in },
|
||||
onEditLongPressBegan: (() -> Void)? = nil
|
||||
@@ -50,6 +52,7 @@ public struct RecordButton: View {
|
||||
self.level = level
|
||||
self.remainingSeconds = remainingSeconds
|
||||
self.isEnabled = isEnabled
|
||||
self.usesLiquidGlass = usesLiquidGlass
|
||||
self.onToggle = onToggle
|
||||
self.onPressingChanged = onPressingChanged
|
||||
self.onEditLongPressBegan = onEditLongPressBegan
|
||||
@@ -104,11 +107,22 @@ public struct RecordButton: View {
|
||||
.frame(width: Layout.outerRing, height: Layout.outerRing)
|
||||
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(discGradient)
|
||||
Circle()
|
||||
.stroke(Color.white.opacity(0.16), lineWidth: 1)
|
||||
.blendMode(.overlay)
|
||||
if usesLiquidGlass {
|
||||
Circle()
|
||||
.fill(.clear)
|
||||
.glassEffect(
|
||||
.regular
|
||||
.tint(glassTint)
|
||||
.interactive(),
|
||||
in: .circle
|
||||
)
|
||||
} else {
|
||||
Circle()
|
||||
.fill(discGradient)
|
||||
Circle()
|
||||
.stroke(Color.white.opacity(0.16), lineWidth: 1)
|
||||
.blendMode(.overlay)
|
||||
}
|
||||
|
||||
Group {
|
||||
switch phase {
|
||||
@@ -171,6 +185,19 @@ public struct RecordButton: View {
|
||||
.accessibilityLabel(Text(SharedL10n.string("keyboard.tapToTalkA11y")))
|
||||
}
|
||||
|
||||
private var glassTint: Color {
|
||||
switch phase {
|
||||
case .recording:
|
||||
return recordingTint
|
||||
case .error, .idleUnavailable:
|
||||
return palette.warning.opacity(0.85)
|
||||
case .preparing, .processing:
|
||||
return palette.surfaceElevated
|
||||
case .idleReady:
|
||||
return palette.accent
|
||||
}
|
||||
}
|
||||
|
||||
private var isIdle: Bool {
|
||||
switch phase {
|
||||
case .idleReady, .idleUnavailable:
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
// TranslationChip.swift
|
||||
// OSGKeyboard · Shared
|
||||
//
|
||||
// Translation target picker chip shared between keyboard extension and
|
||||
// host-app preview surfaces.
|
||||
|
||||
import SwiftUI
|
||||
|
||||
public struct TranslationChip: View, Equatable {
|
||||
public let palette: ThemePalette
|
||||
public let targetLocaleId: String
|
||||
public let onSelect: (String) -> Void
|
||||
|
||||
public init(
|
||||
palette: ThemePalette,
|
||||
targetLocaleId: String,
|
||||
onSelect: @escaping (String) -> Void
|
||||
) {
|
||||
self.palette = palette
|
||||
self.targetLocaleId = targetLocaleId
|
||||
self.onSelect = onSelect
|
||||
}
|
||||
|
||||
nonisolated public static func == (lhs: TranslationChip, rhs: TranslationChip) -> Bool {
|
||||
lhs.palette == rhs.palette && lhs.targetLocaleId == rhs.targetLocaleId
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
Menu {
|
||||
ForEach(TranslationLanguageCatalog.all) { language in
|
||||
Button {
|
||||
onSelect(language.id)
|
||||
} label: {
|
||||
if language.id == targetLocaleId {
|
||||
Label(displayLabel(for: language), systemImage: "checkmark")
|
||||
} else {
|
||||
Text(displayLabel(for: language))
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
label
|
||||
}
|
||||
.menuStyle(.button)
|
||||
.accessibilityLabel(Text(SharedL10n.string("keyboard.translation.a11y")))
|
||||
.accessibilityHint(Text(SharedL10n.string("keyboard.translation.a11yHint")))
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var label: some View {
|
||||
let target = TranslationLanguageCatalog.resolve(targetLocaleId)
|
||||
let enabled = targetLocaleId != TranslationLanguageCatalog.offLocaleId
|
||||
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: enabled ? "character.bubble" : "character.bubble.fill")
|
||||
Text(chipLabel(target: target, enabled: enabled))
|
||||
Image(systemName: "chevron.down")
|
||||
.font(.system(size: 8, weight: .bold))
|
||||
}
|
||||
.font(TypeStyle.caption2)
|
||||
.foregroundStyle(foreground(enabled: enabled))
|
||||
.padding(.horizontal, Spacing.xs + 2)
|
||||
.padding(.vertical, 6)
|
||||
.frame(minHeight: 28)
|
||||
.background(background(enabled: enabled), in: Capsule())
|
||||
.overlay(Capsule().stroke(stroke(enabled: enabled), lineWidth: 0.5))
|
||||
}
|
||||
|
||||
private func displayLabel(for language: TranslationLanguage) -> String {
|
||||
if language.id == TranslationLanguageCatalog.offLocaleId {
|
||||
return SharedL10n.string("keyboard.translation.offMenu")
|
||||
}
|
||||
return language.nativeName
|
||||
}
|
||||
|
||||
private func chipLabel(target: TranslationLanguage, enabled: Bool) -> String {
|
||||
if !enabled {
|
||||
return SharedL10n.string("keyboard.translation.chip")
|
||||
}
|
||||
return "→\(shortLabel(for: target))"
|
||||
}
|
||||
|
||||
private func shortLabel(for target: TranslationLanguage) -> String {
|
||||
switch target.id {
|
||||
case "en": return "EN"
|
||||
case "zh-Hans": return "中"
|
||||
case "zh-Hant": return "繁"
|
||||
case "ja": return "日"
|
||||
case "ko": return "韩"
|
||||
case "fr": return "FR"
|
||||
case "de": return "DE"
|
||||
case "es": return "ES"
|
||||
case "ru": return "RU"
|
||||
case "pt": return "PT"
|
||||
default: return target.promptLanguageName
|
||||
}
|
||||
}
|
||||
|
||||
private func foreground(enabled: Bool) -> Color {
|
||||
enabled ? palette.accent : palette.textPrimary
|
||||
}
|
||||
|
||||
private func background(enabled: Bool) -> Color {
|
||||
enabled ? palette.accent.opacity(0.15) : palette.surfaceElevated
|
||||
}
|
||||
|
||||
private func stroke(enabled: Bool) -> Color {
|
||||
enabled ? palette.accent.opacity(0.35) : palette.divider
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user