import Elementary import ElementaryHTMX import Routes import Styleguide struct HeatingBalancePointForm: HTML, Sendable { let mode: HeatingBalancePoint.Mode let heatLossMode: HeatingBalancePoint.HeatLoss.Mode let response: HeatingBalancePoint.Response? init( mode: HeatingBalancePoint.Mode?, heatLossMode: HeatingBalancePoint.HeatLoss.Mode? = nil, response: HeatingBalancePoint.Response? = nil ) { self.mode = mode ?? .thermal self.heatLossMode = heatLossMode ?? .estimated self.response = response } var content: some HTML { div(.class("flex flex-wrap justify-between")) { FormHeader(label: "Balance Point - \(mode.label)", svg: .scale) Toggle( isOn: mode == .thermal, onLabel: HeatingBalancePoint.Mode.thermal.label, onAttributes: .hxDefaults(get: .heatingBalancePoint(.index(mode: .thermal, heatLossMode: heatLossMode))), offLabel: HeatingBalancePoint.Mode.economic.label, offAttributes: .hxDefaults(get: .heatingBalancePoint(.index(mode: .economic, heatLossMode: heatLossMode))) ) .attributes(.class("mb-6")) } form( .hx.post(route: .heatingBalancePoint(.index)), .hx.target("#result") ) { div(.class("space-y-6")) { switch mode { case .thermal: ThermalFields(heatLossMode: heatLossMode) case .economic: div { // FIX: WarningBox("This is still under development and may not be fully functional.") .attributes(.class("mb-6")) } } div { SubmitButton(label: "Calculate Balance Point") } } } div(.id("result")) { if let response { HeatingBalancePointResponse(response: response) } } } struct ThermalFields: HTML, Sendable { let heatLossMode: HeatingBalancePoint.HeatLoss.Mode var content: some HTML { LabeledContent(label: "System Size (Tons)") { Input(id: "systemSize", placeholder: "System size") .attributes(.type(.number), .min("1"), .step("0.5"), .autofocus, .required) } div { div(.class("mb-4")) { h4(.class("text-lg font-bold")) { "Capacities" } p(.class("text-xs text-blue-500")) { "Entering known capacities gives better results, otherwise capacities will be estimated." } } div(.class("grid grid-cols-1 lg:grid-cols-2 gap-4")) { LabeledContent(label: "Capacity @ 47° (BTU/h)") { Input(id: "capacityAt47", placeholder: "Capacity @ 47° (optional)") .attributes(.type(.number), .min("1"), .step("0.5")) } LabeledContent(label: "Capacity @ 17° (BTU/h)") { Input(id: "capacityAt17", placeholder: "Capacity @ 17° (optional)") .attributes(.type(.number), .min("1"), .step("0.5")) } } } HeatLossFields(mode: heatLossMode) } } struct HeatLossFields: HTML, Sendable { let mode: HeatingBalancePoint.HeatLoss.Mode var content: some HTML { div(.id("heatLossFields")) { div(.class("flex flex-wrap justify-between")) { h4(.class("text-lg font-bold")) { "Heat Loss - \(mode.label)" } Toggle( isOn: mode == .estimated, onLabel: HeatingBalancePoint.HeatLoss.Mode.estimated.label, onAttributes: [ .hx.get(route: .heatingBalancePoint(.heatLossFields(mode: .estimated))), .hx.target("#heatLossFields") ], offLabel: HeatingBalancePoint.HeatLoss.Mode.known.label, offAttributes: [ .hx.get(route: .heatingBalancePoint(.heatLossFields(mode: .known))), .hx.target("#heatLossFields") ] ) .attributes(.class("mb-6")) } switch mode { case .known: knownFields case .estimated: simplifiedFields } } } private var simplifiedFields: some HTML { div(.class("grid grid-cols-1 lg:grid-cols-3 gap-4")) { LabeledContent(label: "Building Size (ft²)") { Input(id: "simplifiedHeatLoss", placeholder: "Square feet") .attributes(.type(.number), .min("1"), .step("0.5"), .required) } div { InputLabel(for: "climateZone") { "Climate Zone" } Select(for: ClimateZone.self, id: "climateZone") { $0.rawValue } } LabeledContent(label: "Outdoor Design Temperature (°F)") { Input(id: "heatingDesignTemperature", placeholder: "Design temperature (optional)") .attributes(.type(.number), .step("0.5")) } } } private var knownFields: some HTML { div(.class("grid grid-cols-1 lg:grid-cols-2 gap-4")) { LabeledContent(label: "Heat Loss (BTU/h)") { Input(id: "knownHeatLoss", placeholder: "Heat loss") .attributes(.type(.number), .min("1"), .step("0.5"), .required) } LabeledContent(label: "Outdoor Design Temperature (°F)") { Input(id: "heatingDesignTemperature", placeholder: "Design temperature") .attributes(.type(.number), .step("0.5"), .required) } } } } } struct HeatingBalancePointResponse: HTML, Sendable { let response: HeatingBalancePoint.Response var content: some HTML { ResultContainer(reset: .heatingBalancePoint(.index(mode: response.mode, heatLossMode: nil))) { div( .class(""" w-full rounded-xl shadow-xl bg-blue-100 text-blue-600 border border-blue-600 py-4 """) ) { div(.class("flex")) { SVG(.scale, color: "text-blue-600") .attributes(.class("px-4")) p(.class("font-medium")) { "\(response.mode.label) Balance Point" } } switch response { case let .thermal(result): thermalResult(result) } } WarningBox(warnings: response.warnings) } } func thermalResult(_ result: HeatingBalancePoint.Response.Thermal) -> some HTML { div { VerticalGroup( label: "Balance Point", value: "\(double: result.balancePointTemperature, fractionDigits: 1)", valueLabel: "°F" ) .attributes(.class("mb-8")) div(.class("grid grid-cols-2 space-y-6")) { div { VerticalGroup( label: "Heat Loss - \(result.heatLossMode.label)", value: "\(double: result.heatLoss, fractionDigits: 0)", valueLabel: "BTU/h" ) } div { VerticalGroup( label: "Heating Design Temperature", value: "\(double: result.heatingDesignTemperature, fractionDigits: 0)", valueLabel: "°F" ) } div { VerticalGroup( label: "Capacity @ 47°", value: "\(double: result.capacityAt47, fractionDigits: 0)", valueLabel: "BTU/h" ) } div { VerticalGroup( label: "Capacity @ 17°", value: "\(double: result.capacityAt17, fractionDigits: 0)", valueLabel: "BTU/h" ) } } } } } private extension HeatingBalancePoint.Mode { var label: String { rawValue.capitalized } } private extension HeatingBalancePoint.HeatLoss.Mode { var label: String { rawValue.capitalized } } private extension HeatingBalancePoint.Response { var mode: HeatingBalancePoint.Mode { switch self { case .thermal: return .thermal } } var warnings: [String] { switch self { case let .thermal(result): return result.warnings } } }