mirror of
https://codeberg.org/secana/Forji.git
synced 2026-06-16 05:13:55 -07:00
feat: size the sign-in button as a large control with a stable loading state
The form's primary action rendered at the default control size, which reads small for the screen's one main action. Use .controlSize(.large) so the Login/Save button gets the standard large-CTA height. The loading state emptied the button: the title faded out and a lone spinner floated on a wide prominent capsule, which read as a hollow button rather than work in progress. Keep the button full instead, with a leading spinner and a status label that switches the title to its present-progressive form (Signing in… / Saving…) while the request runs. The spinner and label cross-fade with a blur-replace morph that Reduce Motion drops, the button keeps its prominent appearance by guarding re-entry in handleSave rather than disabling, and the title stays present so the button never resizes mid-action. The file crosses SwiftLint's default 400-line warning; disabled at the top like Seeder.swift.
This commit is contained in:
parent
2a679140e6
commit
18e7bac7e4
1 changed files with 27 additions and 8 deletions
|
|
@ -2,6 +2,7 @@ import ForgejoKit
|
|||
import SwiftData
|
||||
import SwiftUI
|
||||
|
||||
// swiftlint:disable file_length
|
||||
// swiftlint:disable:next type_body_length
|
||||
struct InstanceFormView: View {
|
||||
enum Mode: Identifiable {
|
||||
|
|
@ -23,6 +24,7 @@ struct InstanceFormView: View {
|
|||
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
||||
@Query(sort: \ForgejoInstance.lastUsed, order: .reverse) private var instances: [ForgejoInstance]
|
||||
@State private var authService: AuthenticationService
|
||||
|
||||
|
|
@ -55,6 +57,14 @@ struct InstanceFormView: View {
|
|||
self.mode = mode
|
||||
}
|
||||
|
||||
/// "Login"/"Save" at rest; the present-progressive form while the request runs.
|
||||
private var buttonTitle: String {
|
||||
if isLoading {
|
||||
return isAddMode ? "Signing in…" : "Saving…"
|
||||
}
|
||||
return isAddMode ? "Login" : "Save"
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
|
|
@ -144,21 +154,27 @@ struct InstanceFormView: View {
|
|||
|
||||
Section {
|
||||
Button(action: handleSave) {
|
||||
HStack {
|
||||
Spacer()
|
||||
// A leading spinner with a status label keeps the button
|
||||
// full while signing in, instead of emptying it to a lone
|
||||
// spinner. Reduce Motion drops the morph.
|
||||
HStack(spacing: 8) {
|
||||
if isLoading {
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
} else {
|
||||
Text(isAddMode ? "Login" : "Save")
|
||||
.fontWeight(.semibold)
|
||||
.controlSize(.regular)
|
||||
.tint(.white)
|
||||
.transition(.blurReplace)
|
||||
}
|
||||
Spacer()
|
||||
Text(buttonTitle)
|
||||
.fontWeight(.semibold)
|
||||
.contentTransition(.opacity)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.animation(reduceMotion ? nil : .smooth(duration: 0.3), value: isLoading)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.controlSize(.large)
|
||||
.disabled(
|
||||
isLoading || serverURL.isEmpty
|
||||
serverURL.isEmpty
|
||||
|| (authMode == .credentials
|
||||
&& (username.isEmpty || (isAddMode && password.isEmpty)
|
||||
|| (needsOTP && otpCode.isEmpty)))
|
||||
|
|
@ -242,6 +258,9 @@ struct InstanceFormView: View {
|
|||
|
||||
// swiftlint:disable:next function_body_length cyclomatic_complexity
|
||||
private func handleSave() {
|
||||
// The button keeps its prominent look while loading; guard re-entry here.
|
||||
guard !isLoading else { return }
|
||||
|
||||
if !name.isEmpty {
|
||||
let conflict = instances.first { inst in
|
||||
let instId = inst.serverURL + inst.username
|
||||
|
|
|
|||
Loading…
Reference in a new issue