Both the first-launch wizard's TCC stage and the HUD banner panel were
showing tail-truncated body text (Grant Permissions descriptions ending
in "...", Limited hotkey mode body cut at "The hotke..."). Two layered
SwiftUI layout bugs:
1. The body Text inside row HStacks (`VStack(title+body) + Spacer +
button`) didn't have `.fixedSize(horizontal: false, vertical: true)`,
so when the trailing button was wide ("Denied — Open System Settings")
SwiftUI's HStack layout compressed Text and chose tail-truncation
over multi-line wrap.
2. The wizard's outer frame was `560×440`, leaving only ~318h for
stageBody after header(64) + dividers(2) + footer(56). The TCC stage
with 4 permission rows needed ≥520h. The vertical squeeze cascaded
into more aggressive Text compression and clipped the bottom Back row.
Fixes:
- WizardView: bump frame to 600×640.
- WizardStageTCCView: `.fixedSize(horizontal: false, vertical: true)` on
description + each row's body. `.frame(maxWidth: .infinity, alignment:
.leading)` on inner row VStacks so they receive the available width
before the trailing button claims it. Spacer minLength bumped to 16
for breathing room between body text and button.
- HUDBanner: same `.fixedSize` + `.frame(maxWidth:)` pair on the title
+body VStack. The 360pt-wide banner needed it most because
`HUDBannerPanel.positionTopTrailing` re-reads the SwiftUI fittingSize
on host — without wrapping, the panel was sized to a single-line body
and forced all subsequent body text to truncate.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
101 lines
3.1 KiB
Swift
101 lines
3.1 KiB
Swift
import SwiftUI
|
|
import Keychain
|
|
import Shell
|
|
|
|
/// Three-stage root view per UI-SPEC Surface 1 (api key → tcc → hotkey).
|
|
/// Header shows stage dots; footer offers Back (disabled on Stage 1). The
|
|
/// actual modal/non-modal distinction lives at the hosting
|
|
/// `OnboardingWizardController`, not here.
|
|
public struct WizardView: View {
|
|
@ObservedObject var state: WizardState
|
|
let keychain: any KeychainStore
|
|
let validator: AnthropicKeyValidator
|
|
let onGrantInputMonitoring: () -> Bool
|
|
let onComplete: () -> Void
|
|
|
|
public init(
|
|
state: WizardState,
|
|
keychain: any KeychainStore,
|
|
validator: AnthropicKeyValidator,
|
|
onGrantInputMonitoring: @escaping () -> Bool,
|
|
onComplete: @escaping () -> Void
|
|
) {
|
|
self.state = state
|
|
self.keychain = keychain
|
|
self.validator = validator
|
|
self.onGrantInputMonitoring = onGrantInputMonitoring
|
|
self.onComplete = onComplete
|
|
}
|
|
|
|
public var body: some View {
|
|
VStack(spacing: 0) {
|
|
header
|
|
Divider()
|
|
stageBody
|
|
Divider()
|
|
footer
|
|
}
|
|
// 440h was too short for the TCC stage (4 permission rows + descriptions
|
|
// + buttons ≈ 520h needed); SwiftUI compressed Text first, producing the
|
|
// tail-truncation visible on first-launch and clipping the Back row.
|
|
.frame(width: 600, height: 640)
|
|
}
|
|
|
|
private var header: some View {
|
|
HStack(spacing: 8) {
|
|
ForEach(WizardStage.allCases.filter { $0 != .complete }, id: \.self) { stage in
|
|
Circle()
|
|
.fill(state.currentStage == stage
|
|
? Color(NSColor.controlAccentColor)
|
|
: Color(NSColor.separatorColor))
|
|
.frame(width: 10, height: 10)
|
|
}
|
|
}
|
|
.frame(height: 64)
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var stageBody: some View {
|
|
switch state.currentStage {
|
|
case .apiKey:
|
|
WizardStageAPIKeyView(
|
|
state: state,
|
|
validator: validator,
|
|
keychain: keychain,
|
|
onAdvance: { state.currentStage = .tcc }
|
|
)
|
|
case .tcc:
|
|
WizardStageTCCView(
|
|
state: state,
|
|
onAdvance: { state.currentStage = .hotkey },
|
|
onGrantInputMonitoring: onGrantInputMonitoring
|
|
)
|
|
case .hotkey:
|
|
WizardStageHotkeyView(state: state, onAdvance: {
|
|
state.currentStage = .complete
|
|
onComplete()
|
|
})
|
|
case .complete:
|
|
Color.clear
|
|
}
|
|
}
|
|
|
|
private var footer: some View {
|
|
HStack {
|
|
if state.currentStage != .apiKey {
|
|
Button("← Back") {
|
|
switch state.currentStage {
|
|
case .tcc: state.currentStage = .apiKey
|
|
case .hotkey: state.currentStage = .tcc
|
|
default: break
|
|
}
|
|
}
|
|
}
|
|
Spacer()
|
|
}
|
|
.padding(.horizontal, 48)
|
|
.frame(height: 56)
|
|
}
|
|
}
|