chore(release): bump version to 2.1.6 (build 131)
Version: CURRENT_PROJECT_VERSION 130 -> 131 (project.yml, AGENTS.md, READMEs, docs/llms.txt). fix(flow): host-app identification now compiles in every build configuration (Debug, TestFlight, Release). The arbiter probe and HostAppIdentity were DEBUG-only, so TestFlight/App Store builds never knew the host bundle id and could not auto-return after a startflow handoff. Debug-only logging and the Home test card are unchanged. Also includes the pending work in the tree: - Home: single account/setup card surface (HomeAccountSurfacePolicy/Views) - Wide screens: voice key row and centred mic placement, iPhone Duo layout - Keyboard: clipboard skill ranking and AI skill layout updates - Debug: account session handoff to the Simulator (+ import script) - Docs: portfolio assets, privacy policy URL, site/README refresh
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
// VoiceKeyRowLayout.swift
|
||||
// OSGKeyboard · Shared
|
||||
//
|
||||
// Keyboard-style voice bottom row for wide hosts (iPad, unfolded iPhone Duo).
|
||||
|
||||
import CoreGraphics
|
||||
|
||||
/// Horizontal geometry of the wide-host voice bottom row:
|
||||
///
|
||||
/// [leadingSide][leadingContext][ send ][trailingContext][trailingSide]
|
||||
///
|
||||
/// - The four icon keys (space / undo / edit / delete) share one width and send
|
||||
/// is exactly twice as wide, so the primary action reads as the main key while
|
||||
/// the row fills edge to edge with uniform gaps. The row is symmetric, so send
|
||||
/// sits on the centre line under the microphone.
|
||||
/// - No globe: the voice surface leaves input-mode switching to the typing tab.
|
||||
/// - Context slots (undo / edit) exist even while the keys are disabled, so
|
||||
/// nothing in the row shifts when they toggle.
|
||||
///
|
||||
/// Coordinates are relative to the row's own leading edge.
|
||||
public struct VoiceKeyRowLayout: Equatable, Sendable {
|
||||
public struct Slot: Equatable, Sendable {
|
||||
public let minX: CGFloat
|
||||
public let width: CGFloat
|
||||
|
||||
public var midX: CGFloat { minX + width / 2 }
|
||||
public var maxX: CGFloat { minX + width }
|
||||
|
||||
public init(minX: CGFloat, width: CGFloat) {
|
||||
self.minX = minX
|
||||
self.width = width
|
||||
}
|
||||
}
|
||||
|
||||
/// Send width as a multiple of one icon key.
|
||||
public static let sendWidthMultiplier: CGFloat = 2
|
||||
/// Below this host width the icon keys would shrink past a comfortable
|
||||
/// touch target, so narrower hosts keep the phone's circle cluster.
|
||||
public static let minimumHostWidth: CGFloat = 500
|
||||
|
||||
public let leadingSide: Slot
|
||||
public let leadingContext: Slot
|
||||
public let send: Slot
|
||||
public let trailingContext: Slot
|
||||
public let trailingSide: Slot
|
||||
|
||||
/// Wide hosts only: iPad-scale metrics, the expanded-phone tier, or a
|
||||
/// landscape iPhone (whose shorter panel has no room for two circle rows),
|
||||
/// with enough width for the row. Portrait iPhones keep the circle
|
||||
/// cluster, which suits one-handed thumb reach.
|
||||
public static func isAvailable(
|
||||
usesIPadMetrics: Bool,
|
||||
usesExpandedPhoneMetrics: Bool,
|
||||
usesCompactHeightPhoneMetrics: Bool,
|
||||
hostWidth: CGFloat
|
||||
) -> Bool {
|
||||
(usesIPadMetrics || usesExpandedPhoneMetrics || usesCompactHeightPhoneMetrics)
|
||||
&& hostWidth >= minimumHostWidth
|
||||
}
|
||||
|
||||
/// - Parameter rowWidth: The row's width, i.e. the same width the typing bottom row spans.
|
||||
public static func make(rowWidth: CGFloat) -> VoiceKeyRowLayout {
|
||||
let spacing = KeyboardChromeLayout.actionKeySpacing
|
||||
|
||||
// 4 icon keys + one send key worth `sendWidthMultiplier` icon keys,
|
||||
// with 4 gaps between those five.
|
||||
let keySpan = max(0, rowWidth - spacing * 4)
|
||||
let keyWidth = keySpan / (4 + sendWidthMultiplier)
|
||||
|
||||
let leadingSide = Slot(minX: 0, width: keyWidth)
|
||||
let leadingContext = Slot(minX: leadingSide.maxX + spacing, width: keyWidth)
|
||||
let send = Slot(minX: leadingContext.maxX + spacing, width: keyWidth * sendWidthMultiplier)
|
||||
let trailingContext = Slot(minX: send.maxX + spacing, width: keyWidth)
|
||||
let trailingSide = Slot(minX: trailingContext.maxX + spacing, width: keyWidth)
|
||||
|
||||
return VoiceKeyRowLayout(
|
||||
leadingSide: leadingSide,
|
||||
leadingContext: leadingContext,
|
||||
send: send,
|
||||
trailingContext: trailingContext,
|
||||
trailingSide: trailingSide
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// VoiceMicPlacement.swift
|
||||
// OSGKeyboard · Shared
|
||||
//
|
||||
// Vertical placement of the voice surface's mic row. Every shell height
|
||||
// (phone, unfolded Duo, landscape, iPad narrow / wide) leaves a different
|
||||
// amount of free height between the top bar and the bottom row; this splits it
|
||||
// into the context slot above the mic and the gap below it with one rule, so
|
||||
// the mic sits centred in the area below the top bar on every device.
|
||||
|
||||
import CoreGraphics
|
||||
|
||||
public enum VoiceMicPlacement {
|
||||
/// Never let the mic touch the bottom row.
|
||||
public static let minimumGap: CGFloat = 10
|
||||
|
||||
/// Context slot height for a free height (`contextAndGap` = slot + gap).
|
||||
/// Centred means the slot above the mic equals the gap plus the bottom row
|
||||
/// below it, but the slot never drops under `minimumContextHeight` (the
|
||||
/// live transcript) and the gap never under `minimumGap`.
|
||||
public static func contextHeight(
|
||||
contextAndGap: CGFloat,
|
||||
bottomRowHeight: CGFloat,
|
||||
minimumContextHeight: CGFloat
|
||||
) -> CGFloat {
|
||||
let centred = (contextAndGap + bottomRowHeight) / 2
|
||||
let ceiling = max(0, contextAndGap - minimumGap)
|
||||
return min(ceiling, max(minimumContextHeight, centred))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user