import Elementary import ElementaryHTMX import PsychrometricClient import Routes import Styleguide struct HVACSystemPerformanceForm: HTML, Sendable { let response: HVACSystemPerformance.Response? init(response: HVACSystemPerformance.Response? = nil) { self.response = response } var content: some HTML { FormHeader(label: "HVAC System Performance", svg: .thermometerSun) form( // Using index to get the correct path, but really uses the `submit` end-point. .hx.post(route: .hvacSystemPerformance(.index)), .hx.target("#result") ) { div(.class("space-y-6")) { div(.class("grid grid-cols-1 md:grid-cols-3 gap-4")) { LabeledContent(label: "System Size (Tons)") { Input(id: "systemSize", placeholder: "System size") .attributes(.type(.number), .min("1"), .step("0.5"), .autofocus, .required) } LabeledContent(label: "Airflow (CFM)") { Input(id: "airflow", placeholder: "Airflow") .attributes(.type(.number), .min("1"), .step("0.5"), .required) } LabeledContent(label: "Altitude (ft.)") { Input(id: "altitude", placeholder: "Project altitude (Optional)") .attributes(.type(.number), .min("1"), .step("0.5")) } } div(.class("grid grid-cols-1 md:grid-cols-2 gap-8")) { div(.class("space-y-4")) { h3(.class("text-lg font-medium")) { "Return Air" } LabeledContent(label: "Dry Bulb (°F)") { Input(id: "returnAirTemperature", placeholder: "Return temperature") } LabeledContent(label: "Indoor Humdity (%)") { Input(id: "returnAirHumidity", placeholder: "Return humidity") .attributes(.type(.number), .step("0.1"), .min("0.1"), .required) } } div(.class("space-y-4")) { h3(.class("text-lg font-medium")) { "Supply Air" } LabeledContent(label: "Dry Bulb (°F)") { Input(id: "supplyAirTemperature", placeholder: "Supply temperature") } LabeledContent(label: "Indoor Humdity (%)") { Input(id: "supplyAirHumidity", placeholder: "Supply humidity") .attributes(.type(.number), .step("0.1"), .min("0.1"), .required) } } } div { SubmitButton(label: "Calculate Performance") } } } div(.id("result")) { if let response { HVACSystemPerformanceResult(response: response) } } } } struct HVACSystemPerformanceResult: HTML, Sendable { let response: HVACSystemPerformance.Response var content: some HTML { ResultContainer(reset: .hvacSystemPerformance(.index)) { // ResultContainer { // Capacities div(.class("grid grid-cols-1 lg:grid-cols-3 gap-4")) { CapacityContainer(title: "Total Capacity", capacity: response.capacity.total, shr: nil) .attributes(.class("text-blue-600 bg-blue-100 border border-blue-600 p-6")) CapacityContainer(title: "Sensible Capacity", capacity: response.capacity.sensible, shr: response.capacity.shr) .attributes(.class("text-green-600 bg-green-100 border border-green-600 p-6")) CapacityContainer(title: "Latent Capacity", capacity: response.capacity.latent, shr: nil) .attributes(.class("text-purple-600 bg-purple-100 border border-purple-600 p-6")) } // System Metrics SystemMetricsView(metrics: response.systemMetrics, shr: response.capacity.shr) .attributes(.class("mt-8 border rounded-xl shadow-lg")) // Psychrometric properties. div(.class("grid grid-cols-1 md:grid-cols-2 gap-6 mt-8")) { PsychrometricPropertiesGrid(title: "Return Air Properties", properties: response.returnAirProperties) PsychrometricPropertiesGrid(title: "Supply Air Properties", properties: response.supplyAirProperties) } } } private struct CapacityContainer: HTML, Sendable { let title: String let capacity: Double let shr: Double? var tons: Double { capacity / 12000 } var content: some HTML { div(.class("rounded-xl")) { h4(.class("text-lg font-semibold mb-2")) { title } div(.class("text-3xl font-bold")) { "\(double: tons, fractionDigits: 1)" span(.class("text-xl ps-2")) { "Tons" } } div(.class("text-sm mt-1")) { "\(double: capacity, fractionDigits: 1) BTU/h" } } } } private struct SystemMetricsView: HTML, Sendable { let metrics: HVACSystemPerformance.SystemMetrics let shr: Double var content: some HTML { div { h4(.class(""" text-lg font-semibold flex justify-center py-2 mb-4 text-blue-600 dark:text-slate-300 """)) { "System Performance Metrics" } div(.class("flex justify-between items-center p-4")) { div(.class("space-y-3")) { LabeledMetric(label: "Airflow per Ton", value: metrics.cfmPerTon, units: "CFM/ton") .attributes(.class("mb-8")) div { LabeledMetric(label: "Temperature Split", value: metrics.actualTemperatureSplit, units: "°F") p(.class("text-xs")) { "Target: \(double: metrics.targetTemperatureSplit) °F" } } } div(.class("space-y-3")) { LabeledMetric(label: "Moisture Removal", value: metrics.condensationRateGallonsPerHour, units: "gal/h") .attributes(.class("mb-8")) LabeledMetric(label: "Sensible Heat Ratio", value: shr, units: "%") } } if shr < 0.7 { WarningBox("Low sensible heat ratio may indicate excessive dehumidification or low airflow.") .attributes(.class("mb-4 mx-8")) } } } private struct LabeledMetric: HTML, Sendable { let label: String let value: Double let units: String let fractionDigits: Int init( label: String, value: Double, units: String, fractionDigits: Int = 1 ) { self.label = label self.value = value self.units = units self.fractionDigits = fractionDigits } var content: some HTML { div { span(.class("text-sm")) { label } p(.class("text-2xl font-semibold")) { "\(double: value, fractionDigits: fractionDigits)" span(.class("text-base font-normal ps-2")) { units } } } } } } private struct PsychrometricPropertiesGrid: HTML, Sendable { let title: String let properties: PsychrometricProperties var content: some HTML { div(.class("rounded-xl border border-blue-600 dark:border-gray-400 bg-blue-100 dark:bg-slate-700 p-4")) { h4(.class(""" text-lg font-semibold flex justify-center py-2 mb-4 text-blue-600 dark:text-slate-300 """)) { title } PsychrometricPropertiesView(properties: properties) .attributes(.class("grid grid-cols-1 gap-4")) } } } }