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
161 lines
7.7 KiB
Swift
161 lines
7.7 KiB
Swift
// KeyboardChromeLayout.swift
|
||
// OSGKeyboard · Shared
|
||
//
|
||
// Cross-surface dimensions that must stay identical in voice and typing modes.
|
||
|
||
import CoreGraphics
|
||
|
||
public enum KeyboardChromeLayout {
|
||
public static let totalHeight: CGFloat = 281
|
||
public static let actionKeyHeight: CGFloat = 50
|
||
/// Shared capsule action height for unified Send and edit-mode controls.
|
||
public static let assistantActionCapsuleHeight: CGFloat = actionKeyHeight
|
||
public static let actionKeyCornerRadius: CGFloat = 10
|
||
/// Shared spacing for every bottom action row. iPad uses a custom globe
|
||
/// slot; iPhone relies on the system-provided switch below the keyboard.
|
||
public static let actionKeySpacing: CGFloat = 8
|
||
/// Globe (🌐) key — small, icon-only.
|
||
public static let globeActionKeyFraction: CGFloat = 0.12
|
||
/// Outer side key — pageSwitch / delete.
|
||
public static let sideActionKeyFraction: CGFloat = 0.18
|
||
/// Center key — space (typing) or return (voice). Widest in the row.
|
||
public static let centerActionKeyFraction: CGFloat = 0.50
|
||
/// Other outer side key — return (typing) or space/delete (voice).
|
||
public static let side2ActionKeyFraction: CGFloat = 0.20
|
||
/// iPad typing bottom row: `[globe · 123 · , · space · . · return]`.
|
||
///
|
||
/// The four-slot phone row gives the centre key 50% of the width, which on
|
||
/// a full-width landscape iPad turns the space bar into a ~660 pt runway.
|
||
/// The system keyboard spends that width on more keys instead, so iPad
|
||
/// gains comma / period and space settles near the system's ~430 pt.
|
||
public static let iPadGlobeFraction: CGFloat = 0.09
|
||
public static let iPadPageSwitchFraction: CGFloat = 0.13
|
||
public static let iPadPunctuationFraction: CGFloat = 0.09
|
||
public static let iPadSpaceFraction: CGFloat = 0.38
|
||
public static let iPadReturnFraction: CGFloat = 0.22
|
||
/// Five gaps separate the six iPad slots.
|
||
public static let iPadActionKeyGapCount: CGFloat = 5
|
||
|
||
public static let horizontalInset: CGFloat = 8
|
||
/// Voice-surface content column cap.
|
||
///
|
||
/// The voice surface is a sparse control cluster over a transparent
|
||
/// background, so filling an iPad's width buys no visual width and only
|
||
/// parks delete/return at the screen edges. The typing surface has the
|
||
/// opposite need (a key grid must fill the width to match the system
|
||
/// keyboard), which is why it no longer shares this constant.
|
||
public static let voiceContentMaxWidth: CGFloat = 700
|
||
|
||
/// Hosts that draw the keyboard-style voice key row span the typing width:
|
||
/// the row, top bar and logo must sit on the same frame as the typing keys,
|
||
/// or they jump sideways on every tab switch.
|
||
public static func voiceContentWidthCap(usesVoiceKeyRow: Bool) -> CGFloat {
|
||
usesVoiceKeyRow ? .infinity : voiceContentMaxWidth
|
||
}
|
||
|
||
/// Width at or above which the typing surface switches to wide-iPad
|
||
/// metrics (taller rows). Chosen to sit between the widest iPad portrait
|
||
/// width (1024 pt on 13") and the narrowest landscape width (1133 pt on
|
||
/// mini), so it tracks real available width rather than device orientation
|
||
/// — Stage Manager and Split View resize into the right bucket for free.
|
||
public static let wideIPadWidthThreshold: CGFloat = 1100
|
||
|
||
/// Uses iPad-scale metrics only when the host is both an iPad and currently
|
||
/// exposes regular horizontal space. Compact Split View / Slide Over keeps
|
||
/// the phone-scale layout, while wide iPhones never opt into iPad metrics.
|
||
public static func usesIPadMetrics(isPad: Bool, hasRegularWidth: Bool) -> Bool {
|
||
isPad && hasRegularWidth
|
||
}
|
||
|
||
/// System-sized iPhone-shaped keys for a phone host with regular space on
|
||
/// both axes — the unfolded iPhone Duo inner display. Landscape Max-size
|
||
/// iPhones are regular × compact and keep the phone metrics.
|
||
public static func usesExpandedPhoneMetrics(
|
||
isPad: Bool,
|
||
hasRegularWidth: Bool,
|
||
hasRegularHeight: Bool
|
||
) -> Bool {
|
||
!isPad && hasRegularWidth && hasRegularHeight
|
||
}
|
||
|
||
/// Shorter phone panel whenever the host is vertically compact: every
|
||
/// iPhone in landscape, including the folded iPhone Duo. Compact-height
|
||
/// iPad windows keep their own metrics.
|
||
public static func usesCompactHeightPhoneMetrics(isPad: Bool, hasCompactHeight: Bool) -> Bool {
|
||
!isPad && hasCompactHeight
|
||
}
|
||
|
||
/// iPad always draws its own globe key. Elsewhere UIKit decides: Face ID
|
||
/// iPhones get a system switch below the keyboard, while the unfolded
|
||
/// iPhone Duo and Home-button iPhones need the keyboard to provide one.
|
||
public static func showsInputModeSwitchKey(
|
||
isPad: Bool,
|
||
systemRequiresSwitchKey: Bool
|
||
) -> Bool {
|
||
isPad || systemRequiresSwitchKey
|
||
}
|
||
|
||
/// Wide metrics need iPad-scale keys *and* enough width to justify them.
|
||
public static func usesWideIPadMetrics(isIPad: Bool, width: CGFloat) -> Bool {
|
||
isIPad && width >= wideIPadWidthThreshold
|
||
}
|
||
|
||
/// Below this width the voice microphone capsule shrinks (Display Zoom on
|
||
/// a small iPhone, iPad Slide Over). Every current iPhone is wider.
|
||
public static let compactVoiceWidthThreshold: CGFloat = 350
|
||
/// Below this width an iPad-metrics voice surface uses its smaller circles.
|
||
public static let compactIPadVoiceWidthThreshold: CGFloat = 420
|
||
|
||
/// `width == 0` means "not measured yet" and never counts as compact.
|
||
public static func usesCompactVoiceMetrics(width: CGFloat) -> Bool {
|
||
width > 0 && width < compactVoiceWidthThreshold
|
||
}
|
||
|
||
public static func usesCompactIPadVoiceMetrics(isIPad: Bool, width: CGFloat) -> Bool {
|
||
isIPad && width > 0 && width < compactIPadVoiceWidthThreshold
|
||
}
|
||
|
||
/// Splits the width left after three gaps into a 12 / 18 / 50 / 20 row for
|
||
/// layouts that include a globe key, with a wide centre and balanced sides.
|
||
public static func actionKeyWidths(availableWidth: CGFloat) -> (globe: CGFloat, side: CGFloat, center: CGFloat, side2: CGFloat) {
|
||
let keyWidth = max(0, availableWidth - actionKeySpacing * 3)
|
||
return (
|
||
globe: keyWidth * globeActionKeyFraction,
|
||
side: keyWidth * sideActionKeyFraction,
|
||
center: keyWidth * centerActionKeyFraction,
|
||
side2: keyWidth * side2ActionKeyFraction
|
||
)
|
||
}
|
||
|
||
/// iPhone bottom row `[side · center · side2]`. Preserve the established
|
||
/// side/centre balance while redistributing the removed globe slot.
|
||
public static func actionKeyWidthsWithoutGlobe(
|
||
availableWidth: CGFloat
|
||
) -> (side: CGFloat, center: CGFloat, side2: CGFloat) {
|
||
let keyWidth = max(0, availableWidth - actionKeySpacing * 2)
|
||
let fractionTotal = sideActionKeyFraction
|
||
+ centerActionKeyFraction
|
||
+ side2ActionKeyFraction
|
||
return (
|
||
side: keyWidth * sideActionKeyFraction / fractionTotal,
|
||
center: keyWidth * centerActionKeyFraction / fractionTotal,
|
||
side2: keyWidth * side2ActionKeyFraction / fractionTotal
|
||
)
|
||
}
|
||
|
||
/// Six-slot iPad variant of `actionKeyWidths`.
|
||
public static func iPadActionKeyWidths(
|
||
availableWidth: CGFloat
|
||
) -> (globe: CGFloat, pageSwitch: CGFloat, comma: CGFloat, space: CGFloat, period: CGFloat, return: CGFloat) {
|
||
let keyWidth = max(0, availableWidth - actionKeySpacing * iPadActionKeyGapCount)
|
||
return (
|
||
globe: keyWidth * iPadGlobeFraction,
|
||
pageSwitch: keyWidth * iPadPageSwitchFraction,
|
||
comma: keyWidth * iPadPunctuationFraction,
|
||
space: keyWidth * iPadSpaceFraction,
|
||
period: keyWidth * iPadPunctuationFraction,
|
||
return: keyWidth * iPadReturnFraction
|
||
)
|
||
}
|
||
}
|