228 lines
6.9 KiB
Swift
228 lines
6.9 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)
|
|
Toggle(
|
|
isOn: mode == .knownAirflow,
|
|
onLabel: "Known Airflow",
|
|
onAttributes: .hxDefaults(get: .roomPressure(.index(mode: .knownAirflow))),
|
|
offLabel: "Measured Pressure",
|
|
offAttributes: .hxDefaults(get: .roomPressure(.index(mode: .measuredPressure)))
|
|
)
|
|
.attributes(.class("mb-6"))
|
|
}
|
|
|
|
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" }
|
|
// TODO: Use Styleguide.Select
|
|
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 .knownAirflow: return "Known Airflow"
|
|
case .measuredPressure: return "Measured Pressure"
|
|
}
|
|
}
|
|
}
|