瀏覽代碼

Merge pull request #757 from Sjoerd-Bo3/feat/dev-medtrum

Align Branch: Feat/dev-medtrum with Dev again
Sjoerd Bozon 9 月之前
父節點
當前提交
4d86d2dcb7

+ 1 - 1
Config.xcconfig

@@ -19,7 +19,7 @@ TRIO_APP_GROUP_ID = group.org.nightscout.$(DEVELOPMENT_TEAM).trio.trio-app-group
 
 
 // The developers set the version numbers, please leave them alone
 // The developers set the version numbers, please leave them alone
 APP_VERSION = 0.5.1
 APP_VERSION = 0.5.1
-APP_DEV_VERSION = 0.5.1.18
+APP_DEV_VERSION = 0.5.1.20
 APP_BUILD_NUMBER = 1
 APP_BUILD_NUMBER = 1
 COPYRIGHT_NOTICE =
 COPYRIGHT_NOTICE =
 
 

+ 10 - 4
Trio/Sources/Localizations/Main/Localizable.xcstrings

@@ -13721,7 +13721,7 @@
         }
         }
       }
       }
     },
     },
-    "• Manual boluses you enter yoursef" : {
+    "• Manual boluses you enter yourself" : {
 
 
     },
     },
     "• Manual temporary basal rates you set yourself" : {
     "• Manual temporary basal rates you set yourself" : {
@@ -102971,7 +102971,7 @@
       }
       }
     },
     },
     "g" : {
     "g" : {
-      "comment" : "Gram abbreviation\nThe short unit display string for grams\ngram of carbs",
+      "comment" : "Gram abbreviation\nThe short unit display string for grams\nUnits for carbs\ngram of carbs",
       "localizations" : {
       "localizations" : {
         "bg" : {
         "bg" : {
           "stringUnit" : {
           "stringUnit" : {
@@ -124718,6 +124718,12 @@
         }
         }
       }
       }
     },
     },
+    "Latest dev: %@" : {
+
+    },
+    "Latest dev: Fetching..." : {
+
+    },
     "Latest Raw Algorithm Output" : {
     "Latest Raw Algorithm Output" : {
       "localizations" : {
       "localizations" : {
         "bg" : {
         "bg" : {
@@ -223962,7 +223968,7 @@
       }
       }
     },
     },
     "U" : {
     "U" : {
-      "comment" : "Insulin unit\nInsulin unit abbreviation\nThe short unit display string for international units of insulin",
+      "comment" : "Insulin unit\nInsulin unit abbreviation\nThe short unit display string for international units of insulin\nUnits for bolus amount",
       "localizations" : {
       "localizations" : {
         "bg" : {
         "bg" : {
           "stringUnit" : {
           "stringUnit" : {
@@ -224175,7 +224181,7 @@
       }
       }
     },
     },
     "U/hr" : {
     "U/hr" : {
-      "comment" : "Insulin unit per hour abbreviation",
+      "comment" : "Insulin unit per hour abbreviation\nUnits text for temporary basal rate",
       "localizations" : {
       "localizations" : {
         "bg" : {
         "bg" : {
           "stringUnit" : {
           "stringUnit" : {

+ 2 - 2
Trio/Sources/Modules/Calibrations/View/CalibrationsRootView.swift

@@ -48,9 +48,9 @@ extension Calibrations {
                             TextFieldWithToolBar(
                             TextFieldWithToolBar(
                                 text: $state.newCalibration,
                                 text: $state.newCalibration,
                                 placeholder: "0",
                                 placeholder: "0",
-                                numberFormatter: manualGlucoseFormatter
+                                numberFormatter: manualGlucoseFormatter,
+                                unitsText: state.units.rawValue
                             )
                             )
-                            Text(state.units.rawValue).foregroundColor(.secondary)
                         }
                         }
                         Button {
                         Button {
                             Task {
                             Task {

+ 6 - 6
Trio/Sources/Modules/DataTable/View/CarbEntryEditorView.swift

@@ -138,9 +138,9 @@ struct CarbEntryEditorView: View {
                             text: $editedCarbs,
                             text: $editedCarbs,
                             placeholder: "0",
                             placeholder: "0",
                             keyboardType: .numberPad,
                             keyboardType: .numberPad,
-                            numberFormatter: mealFormatter
+                            numberFormatter: mealFormatter,
+                            unitsText: String(localized: "g", comment: "Units for carbs")
                         )
                         )
-                        Text("g").foregroundStyle(.secondary)
                     }
                     }
 
 
                     if state.settingsManager.settings.useFPUconversion {
                     if state.settingsManager.settings.useFPUconversion {
@@ -150,9 +150,9 @@ struct CarbEntryEditorView: View {
                                 text: $editedProtein,
                                 text: $editedProtein,
                                 placeholder: "0",
                                 placeholder: "0",
                                 keyboardType: .numberPad,
                                 keyboardType: .numberPad,
-                                numberFormatter: mealFormatter
+                                numberFormatter: mealFormatter,
+                                unitsText: String(localized: "g", comment: "Units for carbs")
                             )
                             )
-                            Text("g").foregroundStyle(.secondary)
                         }
                         }
 
 
                         HStack {
                         HStack {
@@ -161,9 +161,9 @@ struct CarbEntryEditorView: View {
                                 text: $editedFat,
                                 text: $editedFat,
                                 placeholder: "0",
                                 placeholder: "0",
                                 keyboardType: .numberPad,
                                 keyboardType: .numberPad,
-                                numberFormatter: mealFormatter
+                                numberFormatter: mealFormatter,
+                                unitsText: String(localized: "g", comment: "Units for carbs")
                             )
                             )
-                            Text("g").foregroundStyle(.secondary)
                         }
                         }
                     }
                     }
 
 

+ 2 - 2
Trio/Sources/Modules/DataTable/View/DataTableRootView.swift

@@ -440,9 +440,9 @@ extension DataTable {
                                     placeholder: " ... ",
                                     placeholder: " ... ",
                                     keyboardType: state.units == .mgdL ? .numberPad : .decimalPad,
                                     keyboardType: state.units == .mgdL ? .numberPad : .decimalPad,
                                     numberFormatter: manualGlucoseFormatter,
                                     numberFormatter: manualGlucoseFormatter,
-                                    initialFocus: true
+                                    initialFocus: true,
+                                    unitsText: state.units.rawValue
                                 )
                                 )
-                                Text(state.units.rawValue).foregroundStyle(.secondary)
                             }
                             }
                         }.listRowBackground(Color.chart)
                         }.listRowBackground(Color.chart)
 
 

+ 1 - 1
Trio/Sources/Modules/GeneralSettings/View/UnitsLimitsSettingsRootView.swift

@@ -75,7 +75,7 @@ extension UnitsLimitsSettings {
 
 
                         VStack(alignment: .leading, spacing: 0) {
                         VStack(alignment: .leading, spacing: 0) {
                             Text("What's NOT limited:")
                             Text("What's NOT limited:")
-                            Text("• Manual boluses you enter yoursef")
+                            Text("• Manual boluses you enter yourself")
                             Text("• Manual temporary basal rates you set yourself")
                             Text("• Manual temporary basal rates you set yourself")
                         }
                         }
                     }
                     }

+ 2 - 2
Trio/Sources/Modules/ManualTempBasal/View/ManualTempBasalRootView.swift

@@ -27,9 +27,9 @@ extension ManualTempBasal {
                             text: $state.rate,
                             text: $state.rate,
                             placeholder: "0",
                             placeholder: "0",
                             numberFormatter: formatter,
                             numberFormatter: formatter,
-                            initialFocus: true
+                            initialFocus: true,
+                            unitsText: String(localized: "U/hr", comment: "Units text for temporary basal rate")
                         )
                         )
-                        Text("U/hr").foregroundColor(.secondary)
                     }
                     }
                     Picker(selection: $state.durationIndex, label: Text("Duration")) {
                     Picker(selection: $state.durationIndex, label: Text("Duration")) {
                         ForEach(0 ..< state.durationValues.count) { index in
                         ForEach(0 ..< state.durationValues.count) { index in

+ 1 - 1
Trio/Sources/Modules/Onboarding/View/OnboardingView+Util.swift

@@ -417,7 +417,7 @@ enum DeliveryLimitSubstep: Int, CaseIterable, Identifiable {
 
 
                 VStack(alignment: .leading, spacing: 0) {
                 VStack(alignment: .leading, spacing: 0) {
                     Text("What's NOT limited:")
                     Text("What's NOT limited:")
-                    Text("• Manual boluses you enter yoursef")
+                    Text("• Manual boluses you enter yourself")
                     Text("• Manual temporary basal rates you set yourself")
                     Text("• Manual temporary basal rates you set yourself")
                 }
                 }
             }
             }

+ 1 - 1
Trio/Sources/Modules/Settings/View/SettingsRootView.swift

@@ -398,7 +398,7 @@ extension Settings {
                     versionInfo.latestVersion = latestVersion
                     versionInfo.latestVersion = latestVersion
                     versionInfo.isUpdateAvailable = isNewer
                     versionInfo.isUpdateAvailable = isNewer
                     versionInfo.isBlacklisted = isBlacklisted
                     versionInfo.isBlacklisted = isBlacklisted
-                    
+
                     // Fetch dev version if not on main branch
                     // Fetch dev version if not on main branch
                     let buildDetails = BuildDetails.shared
                     let buildDetails = BuildDetails.shared
                     if buildDetails.trioBranch != "main" {
                     if buildDetails.trioBranch != "main" {

+ 6 - 6
Trio/Sources/Modules/Treatments/View/MealPreset/AddMealPresetView.swift

@@ -75,9 +75,9 @@ struct AddMealPresetView: View {
                 text: $presetCarbs,
                 text: $presetCarbs,
                 placeholder: "0",
                 placeholder: "0",
                 keyboardType: .numberPad,
                 keyboardType: .numberPad,
-                numberFormatter: mealFormatter
+                numberFormatter: mealFormatter,
+                unitsText: String(localized: "g", comment: "Units for carbs")
             )
             )
-            Text("g").foregroundColor(.secondary)
         }
         }
     }
     }
 
 
@@ -89,9 +89,9 @@ struct AddMealPresetView: View {
                 text: $presetProtein,
                 text: $presetProtein,
                 placeholder: "0",
                 placeholder: "0",
                 keyboardType: .numberPad,
                 keyboardType: .numberPad,
-                numberFormatter: mealFormatter
+                numberFormatter: mealFormatter,
+                unitsText: String(localized: "g", comment: "Units for carbs")
             )
             )
-            Text("g").foregroundColor(.secondary)
         }
         }
         HStack {
         HStack {
             Text("Fat").foregroundColor(.orange)
             Text("Fat").foregroundColor(.orange)
@@ -100,9 +100,9 @@ struct AddMealPresetView: View {
                 text: $presetFat,
                 text: $presetFat,
                 placeholder: "0",
                 placeholder: "0",
                 keyboardType: .numberPad,
                 keyboardType: .numberPad,
-                numberFormatter: mealFormatter
+                numberFormatter: mealFormatter,
+                unitsText: String(localized: "g", comment: "Units for carbs")
             )
             )
-            Text("g").foregroundColor(.secondary)
         }
         }
     }
     }
 
 

+ 8 - 8
Trio/Sources/Modules/Treatments/View/TreatmentsRootView.swift

@@ -93,10 +93,10 @@ extension Treatments {
                         numberFormatter: mealFormatter,
                         numberFormatter: mealFormatter,
                         showArrows: true,
                         showArrows: true,
                         previousTextField: { focusedField = previousField(from: .protein) },
                         previousTextField: { focusedField = previousField(from: .protein) },
-                        nextTextField: { focusedField = nextField(from: .protein) }
+                        nextTextField: { focusedField = nextField(from: .protein) },
+                        unitsText: String(localized: "g", comment: "Units for carbs")
                     )
                     )
                     .focused($focusedField, equals: .protein)
                     .focused($focusedField, equals: .protein)
-                    Text("g").foregroundColor(.secondary)
                 }
                 }
 
 
                 Divider().foregroundStyle(.primary).fontWeight(.bold).frame(width: 10)
                 Divider().foregroundStyle(.primary).fontWeight(.bold).frame(width: 10)
@@ -110,10 +110,10 @@ extension Treatments {
                         numberFormatter: mealFormatter,
                         numberFormatter: mealFormatter,
                         showArrows: true,
                         showArrows: true,
                         previousTextField: { focusedField = previousField(from: .fat) },
                         previousTextField: { focusedField = previousField(from: .fat) },
-                        nextTextField: { focusedField = nextField(from: .fat) }
+                        nextTextField: { focusedField = nextField(from: .fat) },
+                        unitsText: String(localized: "g", comment: "Units for carbs")
                     )
                     )
                     .focused($focusedField, equals: .fat)
                     .focused($focusedField, equals: .fat)
-                    Text("g").foregroundColor(.secondary)
                 }
                 }
             }
             }
         }
         }
@@ -129,13 +129,13 @@ extension Treatments {
                     numberFormatter: mealFormatter,
                     numberFormatter: mealFormatter,
                     showArrows: true,
                     showArrows: true,
                     previousTextField: { focusedField = previousField(from: .carbs) },
                     previousTextField: { focusedField = previousField(from: .carbs) },
-                    nextTextField: { focusedField = nextField(from: .carbs) }
+                    nextTextField: { focusedField = nextField(from: .carbs) },
+                    unitsText: String(localized: "g", comment: "Units for carbs")
                 )
                 )
                 .focused($focusedField, equals: .carbs)
                 .focused($focusedField, equals: .carbs)
                 .onChange(of: state.carbs) {
                 .onChange(of: state.carbs) {
                     handleDebouncedInput()
                     handleDebouncedInput()
                 }
                 }
-                Text("g").foregroundColor(.secondary)
             }
             }
         }
         }
 
 
@@ -331,14 +331,14 @@ extension Treatments {
                                     numberFormatter: formatter,
                                     numberFormatter: formatter,
                                     showArrows: true,
                                     showArrows: true,
                                     previousTextField: { focusedField = previousField(from: .bolus) },
                                     previousTextField: { focusedField = previousField(from: .bolus) },
-                                    nextTextField: { focusedField = nextField(from: .bolus) }
+                                    nextTextField: { focusedField = nextField(from: .bolus) },
+                                    unitsText: String(localized: "U", comment: "Units for bolus amount")
                                 ).focused($focusedField, equals: .bolus)
                                 ).focused($focusedField, equals: .bolus)
                                     .onChange(of: state.amount) {
                                     .onChange(of: state.amount) {
                                         Task {
                                         Task {
                                             await state.updateForecasts()
                                             await state.updateForecasts()
                                         }
                                         }
                                     }
                                     }
-                                Text(" U").foregroundColor(.secondary)
                             }
                             }
 
 
                             HStack {
                             HStack {

+ 15 - 15
Trio/Sources/Services/AppVersionChecker/AppVersionChecker.swift

@@ -142,7 +142,7 @@ final class AppVersionChecker {
             completion(result.currentVersion, result.latestVersion, result.isNewer, result.isBlacklisted)
             completion(result.currentVersion, result.latestVersion, result.isNewer, result.isBlacklisted)
         }
         }
     }
     }
-    
+
     // Refreshes the version information and returns the current state (async version).
     // Refreshes the version information and returns the current state (async version).
     //
     //
     // This method triggers a version check (using cached values if valid or fetching fresh data)
     // This method triggers a version check (using cached values if valid or fetching fresh data)
@@ -175,7 +175,7 @@ final class AppVersionChecker {
             completion(result.0, result.1)
             completion(result.0, result.1)
         }
         }
     }
     }
-    
+
     // Checks for the latest dev version with caching and comparison (async version).
     // Checks for the latest dev version with caching and comparison (async version).
     //
     //
     // This method attempts to use cached dev version data if it is less than 24 hours old and
     // This method attempts to use cached dev version data if it is less than 24 hours old and
@@ -218,7 +218,7 @@ final class AppVersionChecker {
     // - isNewer: `true` if the fetched dev version is newer than the current version.
     // - isNewer: `true` if the fetched dev version is newer than the current version.
     private func fetchDevVersionAndUpdateCache(currentVersion: String) async -> (String?, Bool) {
     private func fetchDevVersionAndUpdateCache(currentVersion: String) async -> (String?, Bool) {
         let versionData = await fetchData(for: .devVersionConfig)
         let versionData = await fetchData(for: .devVersionConfig)
-        
+
         // Parse the dev version from the fetched configuration data
         // Parse the dev version from the fetched configuration data
         let configContents = versionData.flatMap { String(data: $0, encoding: .utf8) }
         let configContents = versionData.flatMap { String(data: $0, encoding: .utf8) }
         let fetchedDevVersion = configContents.flatMap { self.parseDevVersionFromConfig(contents: $0) }
         let fetchedDevVersion = configContents.flatMap { self.parseDevVersionFromConfig(contents: $0) }
@@ -240,9 +240,9 @@ final class AppVersionChecker {
         } ?? false
         } ?? false
 
 
         // Update persisted cache
         // Update persisted cache
-        self.persistedLatestDevVersion = fetchedDevVersion
-        self.latestDevVersionChecked = Date()
-        self.cachedForDevVersion = currentVersion
+        persistedLatestDevVersion = fetchedDevVersion
+        latestDevVersionChecked = Date()
+        cachedForDevVersion = currentVersion
 
 
         return (fetchedDevVersion, isNewer)
         return (fetchedDevVersion, isNewer)
     }
     }
@@ -265,7 +265,7 @@ final class AppVersionChecker {
             completion(result.0, result.1, result.2)
             completion(result.0, result.1, result.2)
         }
         }
     }
     }
-    
+
     // Checks whether there is a new or blacklisted version (async version).
     // Checks whether there is a new or blacklisted version (async version).
     //
     //
     // This method attempts to use cached version data if it is less than 24 hours old and
     // This method attempts to use cached version data if it is less than 24 hours old and
@@ -322,9 +322,9 @@ final class AppVersionChecker {
         // Fetch both data types in parallel
         // Fetch both data types in parallel
         async let versionData = fetchData(for: .versionConfig)
         async let versionData = fetchData(for: .versionConfig)
         async let blacklistData = fetchData(for: .blacklistedVersions)
         async let blacklistData = fetchData(for: .blacklistedVersions)
-        
+
         let (versionDataResult, blacklistDataResult) = await (versionData, blacklistData)
         let (versionDataResult, blacklistDataResult) = await (versionData, blacklistData)
-        
+
         // Parse the version from the fetched configuration data.
         // Parse the version from the fetched configuration data.
         let fetchedVersion = versionDataResult
         let fetchedVersion = versionDataResult
             .flatMap { String(data: $0, encoding: .utf8) }
             .flatMap { String(data: $0, encoding: .utf8) }
@@ -343,10 +343,10 @@ final class AppVersionChecker {
             .contains(currentVersion) ?? false
             .contains(currentVersion) ?? false
 
 
         // Update persisted cache.
         // Update persisted cache.
-        self.persistedLatestVersion = fetchedVersion
-        self.latestVersionChecked = Date()
-        self.currentVersionBlackListed = isBlacklisted
-        self.cachedForVersion = currentVersion
+        persistedLatestVersion = fetchedVersion
+        latestVersionChecked = Date()
+        currentVersionBlackListed = isBlacklisted
+        cachedForVersion = currentVersion
 
 
         return (fetchedVersion, isNewer, isBlacklisted)
         return (fetchedVersion, isNewer, isBlacklisted)
     }
     }
@@ -378,7 +378,7 @@ final class AppVersionChecker {
             return nil
             return nil
         }
         }
     }
     }
-    
+
     // Legacy completion handler version for existing code
     // Legacy completion handler version for existing code
     private func fetchData(for dataType: GitHubDataType, completion: @escaping (Data?) -> Void) {
     private func fetchData(for dataType: GitHubDataType, completion: @escaping (Data?) -> Void) {
         guard let url = URL(string: dataType.url) else {
         guard let url = URL(string: dataType.url) else {
@@ -410,7 +410,7 @@ final class AppVersionChecker {
     private func parseVersionFromConfig(contents: String) -> String? {
     private func parseVersionFromConfig(contents: String) -> String? {
         let lines = contents.split(separator: "\n")
         let lines = contents.split(separator: "\n")
         for line in lines {
         for line in lines {
-            if line.contains("APP_VERSION") && !line.contains("DEV") {
+            if line.contains("APP_VERSION"), !line.contains("DEV") {
                 let components = line.split(separator: "=").map {
                 let components = line.split(separator: "=").map {
                     $0.trimmingCharacters(in: .whitespacesAndNewlines)
                     $0.trimmingCharacters(in: .whitespacesAndNewlines)
                 }
                 }

+ 129 - 115
Trio/Sources/Views/TextFieldWithToolBar.swift

@@ -18,6 +18,8 @@ public struct TextFieldWithToolBar: View {
     var previousTextField: (() -> Void)?
     var previousTextField: (() -> Void)?
     var nextTextField: (() -> Void)?
     var nextTextField: (() -> Void)?
     var initialFocus: Bool
     var initialFocus: Bool
+    var unitsText: String?
+    var unitsTextColor: Color
 
 
     @FocusState private var isFocused: Bool
     @FocusState private var isFocused: Bool
     @State private var localText: String = ""
     @State private var localText: String = ""
@@ -40,7 +42,9 @@ public struct TextFieldWithToolBar: View {
         showArrows: Bool = false,
         showArrows: Bool = false,
         previousTextField: (() -> Void)? = nil,
         previousTextField: (() -> Void)? = nil,
         nextTextField: (() -> Void)? = nil,
         nextTextField: (() -> Void)? = nil,
-        initialFocus: Bool = false
+        initialFocus: Bool = false,
+        unitsText: String? = nil,
+        unitsTextColor: Color = .secondary
     ) {
     ) {
         _text = text
         _text = text
         self.placeholder = placeholder
         self.placeholder = placeholder
@@ -59,150 +63,160 @@ public struct TextFieldWithToolBar: View {
         self.previousTextField = previousTextField
         self.previousTextField = previousTextField
         self.nextTextField = nextTextField
         self.nextTextField = nextTextField
         self.initialFocus = initialFocus
         self.initialFocus = initialFocus
+        self.unitsText = unitsText
+        self.unitsTextColor = unitsTextColor
     }
     }
 
 
     public var body: some View {
     public var body: some View {
-        TextField(placeholder, text: $localText)
-            .focused($isFocused)
-            .multilineTextAlignment(textAlignment)
-            .foregroundColor(textColor)
-            .keyboardType(keyboardType)
-            .toolbar {
-                if isFocused {
-                    ToolbarItemGroup(placement: .keyboard) {
-                        Button(action: {
-                            localText = ""
-                            text = 0
-                            isZeroCleared = true // Mark as cleared to prevent showing "0"
-                            textDidChange?(0)
-                        }) {
-                            Image(systemName: "trash")
-                        }
-
-                        if showArrows {
-                            Button(action: { previousTextField?() }) {
-                                Image(systemName: "chevron.up")
+        HStack {
+            TextField(placeholder, text: $localText)
+                .focused($isFocused)
+                .multilineTextAlignment(textAlignment)
+                .foregroundColor(textColor)
+                .keyboardType(keyboardType)
+                .toolbar {
+                    if isFocused {
+                        ToolbarItemGroup(placement: .keyboard) {
+                            Button(action: {
+                                localText = ""
+                                text = 0
+                                isZeroCleared = true // Mark as cleared to prevent showing "0"
+                                textDidChange?(0)
+                            }) {
+                                Image(systemName: "trash")
                             }
                             }
-                            Button(action: { nextTextField?() }) {
-                                Image(systemName: "chevron.down")
+
+                            if showArrows {
+                                Button(action: { previousTextField?() }) {
+                                    Image(systemName: "chevron.up")
+                                }
+                                Button(action: { nextTextField?() }) {
+                                    Image(systemName: "chevron.down")
+                                }
                             }
                             }
-                        }
 
 
-                        Spacer()
+                            Spacer()
 
 
-                        if isDismissible {
-                            Button(action: { isFocused = false }) {
-                                Image(systemName: "keyboard.chevron.compact.down")
+                            if isDismissible {
+                                Button(action: { isFocused = false }) {
+                                    Image(systemName: "keyboard.chevron.compact.down")
+                                }
                             }
                             }
                         }
                         }
                     }
                     }
                 }
                 }
-            }
-            .onChange(of: isFocused) { _, newValue in
-                if newValue {
-                    textFieldDidBeginEditing?()
-                    // When gaining focus: if the value is zero and was previously cleared,
-                    // keep the text field empty to show placeholder instead of "0"
-                    if isZeroCleared, text == 0 {
-                        localText = ""
-                    }
-                } else {
-                    // When losing focus: handle formatting and validation
-                    if localText.isEmpty {
-                        // If field is empty, maintain zero value but mark as cleared
-                        // so we can show placeholder instead of "0"
-                        text = 0
-                        isZeroCleared = true
-                    } else if let decimal = Decimal(string: localText, locale: numberFormatter.locale) {
-                        if decimal != 0 {
-                            // For non-zero values, format normally and update binding
-                            text = decimal
-                            localText = numberFormatter.string(from: decimal as NSNumber) ?? ""
-                            isZeroCleared = false
-                        } else {
-                            // If user explicitly entered zero, store the value but
-                            // keep display empty to show placeholder
-                            text = 0
+                .onChange(of: isFocused) { _, newValue in
+                    if newValue {
+                        textFieldDidBeginEditing?()
+                        // When gaining focus: if the value is zero and was previously cleared,
+                        // keep the text field empty to show placeholder instead of "0"
+                        if isZeroCleared, text == 0 {
                             localText = ""
                             localText = ""
+                        }
+                    } else {
+                        // When losing focus: handle formatting and validation
+                        if localText.isEmpty {
+                            // If field is empty, maintain zero value but mark as cleared
+                            // so we can show placeholder instead of "0"
+                            text = 0
                             isZeroCleared = true
                             isZeroCleared = true
+                        } else if let decimal = Decimal(string: localText, locale: numberFormatter.locale) {
+                            if decimal != 0 {
+                                // For non-zero values, format normally and update binding
+                                text = decimal
+                                localText = numberFormatter.string(from: decimal as NSNumber) ?? ""
+                                isZeroCleared = false
+                            } else {
+                                // If user explicitly entered zero, store the value but
+                                // keep display empty to show placeholder
+                                text = 0
+                                localText = ""
+                                isZeroCleared = true
+                            }
                         }
                         }
                     }
                     }
                 }
                 }
-            }
-            .onChange(of: localText) { oldValue, newValue in
-                // Reset zero-cleared state as soon as user starts typing anything
-                if !newValue.isEmpty {
-                    isZeroCleared = false
-                }
-
-                // Special handling for backspace operations to maintain decimal format
-                if oldValue.count == newValue.count + 1 {
-                    let decimalSeparator = numberFormatter.decimalSeparator ?? "."
-
-                    // Special case: When backspacing to leave only a decimal point
-                    // e.g., "10.1" -> "10." - Keep decimal separator without adding trailing zero
-                    if newValue.hasSuffix(decimalSeparator) {
-                        if let decimal = Decimal(string: newValue + "0", locale: numberFormatter.locale) {
-                            text = decimal
-                            textDidChange?(decimal)
-                        }
-                        return
+                .onChange(of: localText) { oldValue, newValue in
+                    // Reset zero-cleared state as soon as user starts typing anything
+                    if !newValue.isEmpty {
+                        isZeroCleared = false
                     }
                     }
 
 
-                    // Special case: When backspacing the last digit after a decimal point
-                    // e.g., "10.0" -> "10." - Ensure we keep proper decimal format
-                    if oldValue.contains(decimalSeparator), newValue.contains(decimalSeparator) {
-                        let oldParts = oldValue.components(separatedBy: decimalSeparator)
-                        let newParts = newValue.components(separatedBy: decimalSeparator)
-
-                        // Check if we've removed the last digit after decimal point
-                        if oldParts.count > 1, newParts.count > 1,
-                           oldParts[1].count == 1, newParts[1].isEmpty
-                        {
-                            // Keep proper decimal format by adding trailing zero
-                            localText = newParts[0] + decimalSeparator + "0"
+                    // Special handling for backspace operations to maintain decimal format
+                    if oldValue.count == newValue.count + 1 {
+                        let decimalSeparator = numberFormatter.decimalSeparator ?? "."
 
 
-                            if let decimal = Decimal(string: localText, locale: numberFormatter.locale) {
+                        // Special case: When backspacing to leave only a decimal point
+                        // e.g., "10.1" -> "10." - Keep decimal separator without adding trailing zero
+                        if newValue.hasSuffix(decimalSeparator) {
+                            if let decimal = Decimal(string: newValue + "0", locale: numberFormatter.locale) {
                                 text = decimal
                                 text = decimal
                                 textDidChange?(decimal)
                                 textDidChange?(decimal)
                             }
                             }
                             return
                             return
                         }
                         }
+
+                        // Special case: When backspacing the last digit after a decimal point
+                        // e.g., "10.0" -> "10." - Ensure we keep proper decimal format
+                        if oldValue.contains(decimalSeparator), newValue.contains(decimalSeparator) {
+                            let oldParts = oldValue.components(separatedBy: decimalSeparator)
+                            let newParts = newValue.components(separatedBy: decimalSeparator)
+
+                            // Check if we've removed the last digit after decimal point
+                            if oldParts.count > 1, newParts.count > 1,
+                               oldParts[1].count == 1, newParts[1].isEmpty
+                            {
+                                // Keep proper decimal format by adding trailing zero
+                                localText = newParts[0] + decimalSeparator + "0"
+
+                                if let decimal = Decimal(string: localText, locale: numberFormatter.locale) {
+                                    text = decimal
+                                    textDidChange?(decimal)
+                                }
+                                return
+                            }
+                        }
                     }
                     }
-                }
 
 
-                // Process normal text input changes
-                handleTextChange(oldValue, newValue)
-            }
-            .onChange(of: text) { oldValue, newValue in
-                // Handle external changes to the text binding
-                // (changes not initiated by typing, like programmatic changes)
-                if oldValue != newValue,
-                   Decimal(string: localText, locale: numberFormatter.locale) != newValue
-                {
-                    if newValue == 0, isZeroCleared {
-                        // If value is zero and field was cleared, keep display empty to show placeholder
-                        localText = ""
-                    } else {
-                        // Otherwise format and display the new value
-                        localText = numberFormatter.string(from: newValue as NSNumber) ?? ""
-                        isZeroCleared = false
+                    // Process normal text input changes
+                    handleTextChange(oldValue, newValue)
+                }
+                .onChange(of: text) { oldValue, newValue in
+                    // Handle external changes to the text binding
+                    // (changes not initiated by typing, like programmatic changes)
+                    if oldValue != newValue,
+                       Decimal(string: localText, locale: numberFormatter.locale) != newValue
+                    {
+                        if newValue == 0, isZeroCleared {
+                            // If value is zero and field was cleared, keep display empty to show placeholder
+                            localText = ""
+                        } else {
+                            // Otherwise format and display the new value
+                            localText = numberFormatter.string(from: newValue as NSNumber) ?? ""
+                            isZeroCleared = false
+                        }
                     }
                     }
                 }
                 }
-            }
-            .onAppear {
-                if text != 0 {
-                    // Initialize with formatted non-zero value
-                    localText = numberFormatter.string(from: text as NSNumber) ?? ""
-                    isZeroCleared = false
-                } else {
-                    // For zero values, start with empty field to show placeholder
-                    localText = ""
-                    isZeroCleared = true
+                .onAppear {
+                    if text != 0 {
+                        // Initialize with formatted non-zero value
+                        localText = numberFormatter.string(from: text as NSNumber) ?? ""
+                        isZeroCleared = false
+                    } else {
+                        // For zero values, start with empty field to show placeholder
+                        localText = ""
+                        isZeroCleared = true
+                    }
+                    // Set initial focus if requested
+                    isFocused = initialFocus
                 }
                 }
-                // Set initial focus if requested
-                isFocused = initialFocus
+            if unitsText != nil {
+                Text(unitsText ?? "").foregroundColor(unitsTextColor)
+                    .onTapGesture {
+                        isFocused = true
+                    }
             }
             }
+        }
     }
     }
 
 
     private func handleTextChange(_ oldValue: String, _ newValue: String) {
     private func handleTextChange(_ oldValue: String, _ newValue: String) {