import Elementary import ElementaryHTMX import Routes import Styleguide struct AtticVentilationForm: HTML, Sendable { let response: AtticVentilation.Response? var content: some HTML { FormHeader(label: "Attic Ventilation Calculator", svg: .wind) // Form. form( .hx.post(route: .atticVentilation(.index)), .hx.target("#result") ) { div(.class("space-y-6")) { div { LabeledContent(label: "Pressure Differential: (Pascals - WRT outside)") { Input(id: "pressureDifferential", placeholder: "Pressure differential") .attributes(.type(.number), .step("0.1"), .autofocus, .required) } p(.class("text-sm text-light mt-2")) { "Positive values indicate higher attic pressure" } } div(.class("grid grid-cols-1 md:grid-cols-2 gap-4")) { div { p(.class("font-bold mb-6")) { "Outdoor Conditions" } LabeledContent(label: "Temperature: (°F)") { Input(id: "outdoorTemperature", placeholder: "Outdoor temperature") .attributes(.class("mb-4"), .type(.number), .step("0.1"), .required) } LabeledContent(label: "Dew Point: (°F)") { Input(id: "outdoorDewpoint", placeholder: "Outdoor dewpoint") .attributes(.type(.number), .step("0.1"), .required) } } div { p(.class("font-bold mb-6")) { "Attic Conditions" } LabeledContent(label: "Temperature: (°F)") { Input(id: "atticTemperature", placeholder: "Attic temperature") .attributes(.class("mb-4"), .type(.number), .step("0.1"), .required) } LabeledContent(label: "Dew Point: (°F)") { Input(id: "atticDewpoint", placeholder: "Attic dewpoint") .attributes(.type(.number), .step("0.1"), .required) } } } LabeledContent(label: "Attic Floor Area: (ft²)") { Input(id: "atticFloorArea", placeholder: "Attic floor area") .attributes(.type(.number), .step("0.1"), .min("0.1"), .required) } div(.class("grid grid-cols-1 md:grid-cols-2 gap-4")) { LabeledContent(label: "Existing Intake Area: (ft²)") { Input(id: "existingIntakeArea", placeholder: "Intake area (optional)") .attributes(.class("mb-4"), .type(.number), .step("0.1"), .min("0.1"), .required) } LabeledContent(label: "Existing Exhaust Area: (ft²)") { Input(id: "existingExhaustArea", placeholder: "Exhaust area (optional)") .attributes(.type(.number), .step("0.1"), .min("0.1"), .required) } } div { SubmitButton(label: "Calculate Ventilation") } } } div(.id("result")) { if let response { AtticVentilationResponse(response: response) } } } } struct AtticVentilationResponse: HTML, Sendable { let response: AtticVentilation.Response var content: some HTML { ResultContainer(reset: .atticVentilation(.index)) { div(.class("space-y-6")) { statusView() requiredVentilationView() WarningBox(warnings: response.warnings) Note { """ Calculations are based on standard ventilation guidelines and building codes. Local codes may vary. Consider consulting with a qualified professional for specific recommendations. """ } } } } private func statusView() -> some HTML { div( .class(""" w-full rounded-lg shadow-lg border-2 p-6 \(response.ventilationStatus.borderColor) \(response.ventilationStatus.textColor) \(response.ventilationStatus.backgroundColor) """) ) { div(.class("flex text-2xl mb-6")) { SVG(.thermometerSun, color: response.ventilationStatus.textColor) h4(.class("font-extrabold ms-2")) { "Ventilation Status: \(response.ventilationStatus.rawValue.capitalized)" } } div(.class("grid justify-items-stretch grid-cols-1 md:grid-cols-3")) { group("Temperature Differential", "\(double: response.temperatureDifferential, fractionDigits: 1) °F") group("Dewpoint Differential", "\(double: response.dewpointDifferential, fractionDigits: 1) °F") group("Pressure Differential", "\(double: response.pressureDifferential, fractionDigits: 1) °F") } } } private func requiredVentilationView() -> some HTML { div( .class(""" w-full rounded-lg border p-6 border-blue-600 text-blue-600 bg-blue-100 dark:bg-blue-300 """) ) { div(.class("flex text-2xl mb-6")) { h4(.class("font-extrabold ms-2")) { "Required Ventilation" } } div(.class("grid grid-cols-1 md:grid-cols-2 content-center")) { group("Intake", "\(double: response.requiredVentilationArea.intake, fractionDigits: 1) ft²") group("Exhaust", "\(double: response.requiredVentilationArea.exhaust, fractionDigits: 1) ft²") } if response.recommendations.count > 0 { div(.class("mt-8")) { h4(.class("font-bold")) { "Recommendations:" } ul(.class("list-disc mx-10")) { for recommendation in response.recommendations { li { recommendation } } } } } } } // TODO: Use Styleguid.VerticalGroup private func group(_ label: String, _ value: String) -> some HTML { div(.class("justify-self-center")) { div(.class("grid grid-cols-1 justify-items-center")) { p(.class("font-medium")) { label } h3(.class("text-3xl font-extrabold")) { value } } } } } private extension AtticVentilation.VentilationStatus { var backgroundColor: String { switch self { case .adequate: return "bg-green-200" case .inadequate: return "bg-amber-200" case .critical: return "bg-red-200" } } var borderColor: String { switch self { case .adequate: return "border-green-500" case .inadequate: return "border-amber-500" case .critical: return "border-red-500" } } var textColor: String { switch self { case .adequate: return "text-green-500" case .inadequate: return "text-amber-500" case .critical: return "text-red-500" } } }