Files
swift-hvac-toolbox/Sources/ViewController/Views/RoomPressure.swift

257 lines
8.1 KiB
Swift

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")) {
div(.class("flex flex-wrap justify-between")) {
FormHeader(label: "Room Pressure Calculator - \(mode.label)", svg: .leftRightArrow)
// Mode toggle / buttons.
div(.class("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)
.attributes(.class("mt-6"))
div(.id("result")) {
if let response {
RoomPressureResult(response: response)
}
}
}
}
struct Form: HTML, Sendable {
let mode: RoomPressure.Mode
var content: some HTML<HTMLTag.form> {
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<Body: HTML>: 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<HTMLTag.div> {
div(.class("rounded-xl p-6")) {
h4(.class("text-xl font-bold")) { title }
body
}
}
}
}
private extension RoomPressure.Mode {
var label: String {
switch self {
case let .knownAirflow: return "Known Airflow"
case let .measuredPressure: return "Measured Pressure"
}
}
}