feat: add iOS support and harden receipt scanning
This commit is contained in:
79
app/ios/ScanReceipts/Design/ZenithTextField.swift
Normal file
79
app/ios/ScanReceipts/Design/ZenithTextField.swift
Normal file
@@ -0,0 +1,79 @@
|
||||
import SwiftUI
|
||||
|
||||
/// Input fields per `DESIGN (1).md` → Components → Input Fields: "1px solid
|
||||
/// #E2E8F0 bottom-border only... Use `label-caps` for field labels placed
|
||||
/// strictly above the input." Border thickens to 2px while focused — the
|
||||
/// same "shift, don't lift" interaction rule as `ZenithButtonStyle`.
|
||||
///
|
||||
/// Use this instead of a bare `TextField` + `.textFieldStyle(.roundedBorder)`
|
||||
/// everywhere in the app; `.roundedBorder` fights this design system
|
||||
/// directly (it's rounded and iOS-chrome-grey, the opposite of "sharp,
|
||||
/// monochrome, architectural").
|
||||
struct ZenithTextField: View {
|
||||
let label: String
|
||||
@Binding var text: String
|
||||
var placeholder: String = ""
|
||||
var isSecure = false
|
||||
var keyboardType: UIKeyboardType = .default
|
||||
var textContentType: UITextContentType?
|
||||
var autocapitalization: TextInputAutocapitalization = .sentences
|
||||
var autocorrectionDisabled = false
|
||||
|
||||
@FocusState private var isFocused: Bool
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: ZenithSpacing.unit) {
|
||||
Text(label).zenithLabelCapsStyle()
|
||||
|
||||
Group {
|
||||
if isSecure {
|
||||
SecureField(placeholder, text: $text)
|
||||
} else {
|
||||
TextField(placeholder, text: $text)
|
||||
}
|
||||
}
|
||||
.font(.zenithBodyMd)
|
||||
.foregroundStyle(.zenithText)
|
||||
.keyboardType(keyboardType)
|
||||
.textContentType(textContentType)
|
||||
.textInputAutocapitalization(autocapitalization)
|
||||
.autocorrectionDisabled(autocorrectionDisabled)
|
||||
.focused($isFocused)
|
||||
.padding(.vertical, ZenithSpacing.unit * 2)
|
||||
.overlay(alignment: .bottom) {
|
||||
Rectangle()
|
||||
.fill(isFocused ? Color.zenithBlack : Color.zenithBorder)
|
||||
.frame(height: isFocused ? 2 : 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A numeric variant bound to an optional `Double`, for amount fields — free
|
||||
/// text with comma-or-dot decimal entry (matches how receipts are actually
|
||||
/// typed in German usage) rather than a strict `TextField(value:format:)`
|
||||
/// that rejects a comma outright.
|
||||
struct ZenithAmountField: View {
|
||||
let label: String
|
||||
@Binding var value: Double?
|
||||
var placeholder: String = "0,00"
|
||||
|
||||
private var textProxy: Binding<String> {
|
||||
Binding(
|
||||
get: { value.map { String(format: "%.2f", $0) } ?? "" },
|
||||
set: { newValue in
|
||||
let normalized = newValue.replacingOccurrences(of: ",", with: ".")
|
||||
value = Double(normalized)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZenithTextField(
|
||||
label: label,
|
||||
text: textProxy,
|
||||
placeholder: placeholder,
|
||||
keyboardType: .decimalPad
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user