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:
Rocky
2026-08-13 01:00:51 +08:00
parent fd6e0d3e7e
commit 9f308fadd2
202 changed files with 10897 additions and 5962 deletions
@@ -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
}
}