import Elementary import ElementaryHTMX import Routes import Styleguide struct RoomPressureForm: HTML, Sendable { let response: RoomPressure.Response? let mode: RoomPressure.Mode init(mode: RoomPressure.Mode? = nil, response: RoomPressure.Response? = nil) { self.mode = mode ?? .knownAirflow self.response = response } var content: some HTML { div(.class("relative")) { FormHeader(label: "Room Pressure Calculator", svg: .leftRightArrow) // Mode toggle / buttons. div(.class("absolute top-0 right-0 flex items-center gap-x-0")) { switch mode { case .knownAirflow: SecondaryButton(label: "Known Airflow") .attributes(.class("rounded-s-lg")) .attributes(.disabled, when: mode == .knownAirflow) .attributes( .hx.get(route: .roomPressure(.index(mode: .knownAirflow))), .hx.target("#content"), when: mode == .measuredPressure ) PrimaryButton(label: "Measured Pressure") .attributes(.class("rounded-e-lg")) .attributes( .hx.get(route: .roomPressure(.index(mode: .measuredPressure))), .hx.target("#content"), when: mode == .knownAirflow ) case .measuredPressure: PrimaryButton(label: "Known Airflow") .attributes(.class("rounded-s-lg")) .attributes( .hx.get(route: .roomPressure(.index(mode: .knownAirflow))), .hx.target("#content"), when: mode == .measuredPressure ) SecondaryButton(label: "Measured Pressure") .attributes(.class("rounded-e-lg")) .attributes(.disabled, when: mode == .measuredPressure) .attributes( .hx.get(route: .roomPressure(.index(mode: .measuredPressure))), .hx.target("#content"), when: mode == .knownAirflow ) } } Form(mode: mode) div(.id("result")) { if let response { RoomPressureResult(response: response) } } } } struct Form: HTML, Sendable { let mode: RoomPressure.Mode var content: some HTML { form( .hx.post(route: .roomPressure(.index)), .hx.target("#result") ) { div(.class("space-y-6")) { LabeledContent(label: pressureLabel) { // NB: using .attributes(..., when:...) not working, so using a switch statement. switch mode { case .knownAirflow: Input(id: pressureID, placeholder: pressurePlaceholder) .attributes( .type(.number), .step("0.1"), .min("0.1"), .max("3.0"), .autofocus, .required ) case .measuredPressure: Input(id: pressureID, placeholder: pressurePlaceholder) .attributes(.type(.number), .step("0.1"), .min("0.1"), .autofocus, .required) } } DoorDetails() if mode == .knownAirflow { LabeledContent(label: "Supply Airflow (CFM)") { Input(id: "supplyAirflow", placeholder: "Airflow") .attributes(.type(.number), .step("0.1"), .min("0.1"), .required) } } PreferredGrilleHeight() div { SubmitButton(label: "Calculate Return Path Size") } } } } private var pressureLabel: String { switch mode { case .knownAirflow: return "Target Room Pressure (Pascals)" case .measuredPressure: return "Measured Room Pressure (Pascals)" } } private var pressureID: String { switch mode { case .knownAirflow: return "targetRoomPressure" case .measuredPressure: return "measuredRoomPressure" } } private var pressurePlaceholder: String { switch mode { case .knownAirflow: return "Room pressure (max 3 pa.)" case .measuredPressure: return "Measured pressure" } } } private struct DoorDetails: HTML, Sendable { var content: some HTML { div(.class("grid grid-cols-1 lg:grid-cols-2 gap-6")) { LabeledContent(label: "Door Width (in.)") { Input(id: "doorWidth", placeholder: "Width") .attributes(.type(.number), .step("0.1"), .min("0.1"), .required) } LabeledContent(label: "Door Height (in.)") { Input(id: "doorHeight", placeholder: "Height") .attributes(.type(.number), .step("0.1"), .min("0.1"), .required) } } LabeledContent(label: "Door Undercut (in.)") { Input(id: "doorUndercut", placeholder: "Undercut height") .attributes(.type(.number), .step("0.1"), .min("0.1"), .required) } } } private struct PreferredGrilleHeight: HTML, Sendable { var content: some HTML { InputLabel(for: "preferredGrilleHeight") { "Preferred Grille Height" } select( .id("preferredGrilleHeight"), .name("preferredGrilleHeight"), .class("w-full px-4 py-2 rounded-md border") ) { for height in RoomPressure.CommonReturnGrilleHeight.allCases { option(.value("\(height.rawValue)")) { height.label } } } } } } struct RoomPressureResult: HTML, Sendable { let response: RoomPressure.Response var content: some HTML { ResultContainer(reset: .roomPressure(.index(mode: response.mode))) { div(.class("grid grid-cols-1 lg:grid-cols-2 gap-4")) { RoundedContainer(title: "Return / Transfer Grille") { div(.class("flex justify-between mt-6")) { span(.class("font-semibold")) { "Standard Size:" } span { """ \(response.grilleSize.width)" x \(response.grilleSize.height)" """ } } div(.class("flex justify-between mt-3")) { span(.class("font-semibold")) { "Required Net Free Area:" } span { """ \(double: response.grilleSize.area, fractionDigits: 1) in """ sup { "2" } } } div(.class("mt-8 text-sm")) { span(.class("font-semibold")) { "Note: " } span { "Select a grille with at least \(double: response.grilleSize.area, fractionDigits: 1) in" sup { "2" } span { " net free area." } } } } .attributes(.class("bg-blue-100 border border-blue-600 text-blue-600")) RoundedContainer(title: "Return / Transfer Duct") { div(.class("flex justify-between mt-6")) { span(.class("font-semibold")) { "Standard Size:" } span { "\(response.ductSize.diameter)\"" } } div(.class("flex justify-between mt-3")) { span(.class("font-semibold")) { "Air Velocity:" } span { "\(double: response.ductSize.velocity, fractionDigits: 1) FPM" } } } .attributes(.class("bg-purple-100 border border-purple-600 text-purple-600")) } WarningBox(warnings: response.warnings) Note { """ Calculations are based on a target velocity of 400 FPM for return/transfer air paths. The required net free area is the minimum needed - select a grille that meets or exceeds this value. Verify manufacturer specifications for actual net free area of selected grilles. """ } } } private struct RoundedContainer: HTML, Sendable where Body: Sendable { let title: String let body: Body init(title: String, @HTMLBuilder body: () -> Body) { self.title = title self.body = body() } var content: some HTML { div(.class("rounded-xl p-6")) { h4(.class("text-xl font-bold")) { title } body } } } }