feat(keyboard): add clipboard AI skills, hint keywords, and voice session fixes

Idle chips show entities with category icons; a fresh copy surfaces Reply/Summarize/Translate; abort/cancel/empty-tap no longer leave the mic stuck.
This commit is contained in:
Rocky
2026-08-13 15:47:32 +08:00
parent 9f308fadd2
commit 631617ee53
48 changed files with 1627 additions and 345 deletions
@@ -10,6 +10,8 @@ import OSGKeyboardShared
private enum ToolbarButtonMetrics {
static let iconSize: CGFloat = 14
/// Slightly larger glyph so a 52 pt circular glass key does not look sparse.
static let circleIconSize: CGFloat = 17
static let titleSize: CGFloat = 16
static let cornerRadius: CGFloat = KeyboardChromeLayout.actionKeyCornerRadius
static let spaceBarCapsuleWidth: CGFloat = 31
@@ -201,6 +203,7 @@ struct RectangularToolbarButton: View {
let disabled: Bool
let isSend: Bool
let usesLiquidGlass: Bool
let usesCircleGlass: Bool
/// Settings General Haptics; space / return use `.action` role.
var hapticIntensity: KeyboardHapticIntensity = .off
let action: () -> Void
@@ -210,6 +213,7 @@ struct RectangularToolbarButton: View {
label: String,
disabled: Bool = false,
usesLiquidGlass: Bool = false,
usesCircleGlass: Bool = false,
hapticIntensity: KeyboardHapticIntensity = .off,
action: @escaping () -> Void
) {
@@ -220,6 +224,7 @@ struct RectangularToolbarButton: View {
self.disabled = disabled
self.isSend = false
self.usesLiquidGlass = usesLiquidGlass
self.usesCircleGlass = usesCircleGlass
self.hapticIntensity = hapticIntensity
self.action = action
}
@@ -239,6 +244,7 @@ struct RectangularToolbarButton: View {
self.disabled = disabled
self.isSend = isSend
self.usesLiquidGlass = usesLiquidGlass
self.usesCircleGlass = false
self.hapticIntensity = hapticIntensity
self.action = action
self.title = title
@@ -259,6 +265,7 @@ struct RectangularToolbarButton: View {
self.disabled = disabled
self.isSend = false
self.usesLiquidGlass = usesLiquidGlass
self.usesCircleGlass = false
self.hapticIntensity = hapticIntensity
self.action = action
}
@@ -267,7 +274,7 @@ struct RectangularToolbarButton: View {
var body: some View {
buttonSurface
.contentShape(Rectangle())
.contentShape(usesCircleGlass ? AnyShape(Circle()) : AnyShape(Rectangle()))
.gesture(pressGesture)
.opacity(disabled ? 0.38 : 1)
.allowsHitTesting(!disabled)
@@ -282,13 +289,7 @@ struct RectangularToolbarButton: View {
Color.clear
buttonContent
}
.glassEffect(
.regular.interactive(),
in: RoundedRectangle(
cornerRadius: ToolbarButtonMetrics.cornerRadius,
style: .continuous
)
)
.modifier(ToolbarLiquidGlass(isCircle: usesCircleGlass))
// The custom press gesture fires on touch-down; mirror that state
// visually while Liquid Glass supplies its native light response.
.scaleEffect(isPressing ? 0.97 : 1)
@@ -312,7 +313,12 @@ struct RectangularToolbarButton: View {
.frame(width: ToolbarButtonMetrics.spaceBarCapsuleWidth, height: 3)
} else if let systemName {
Image(systemName: systemName)
.font(.system(size: ToolbarButtonMetrics.iconSize, weight: .semibold))
.font(.system(
size: usesCircleGlass
? ToolbarButtonMetrics.circleIconSize
: ToolbarButtonMetrics.iconSize,
weight: .semibold
))
.foregroundStyle(buttonForeground)
} else if let title {
Text(title)
@@ -340,3 +346,21 @@ struct RectangularToolbarButton: View {
}
}
}
private struct ToolbarLiquidGlass: ViewModifier {
let isCircle: Bool
func body(content: Content) -> some View {
if isCircle {
content.glassEffect(.regular.interactive(), in: Circle())
} else {
content.glassEffect(
.regular.interactive(),
in: RoundedRectangle(
cornerRadius: ToolbarButtonMetrics.cornerRadius,
style: .continuous
)
)
}
}
}