feat: Initial filter pressure drop views, calculations need implemented.

This commit is contained in:
2025-03-02 21:51:52 -05:00
parent a8022ec80a
commit 67488e06a9
17 changed files with 610 additions and 97 deletions

File diff suppressed because one or more lines are too long

View File

@@ -40,6 +40,11 @@ extension ApiController: DependencyKey {
logger.debug("Calculating dehumidifier size: \(request)")
return try await request.respond(logger)
case let .calculateFilterPressureDrop(request):
logger.debug("Calculating filter pressure drop: \(request)")
// FIX:
fatalError()
case let .calculateHVACSystemPerformance(request):
logger.debug("Calculating hvac system performance: \(request)")
return try await request.respond(logger: logger)

View File

@@ -1,8 +1,10 @@
public enum ClimateZone: String, Codable, Equatable, Sendable {
case dry
public enum ClimateZone: String, CaseIterable, Codable, Equatable, Sendable {
// NOTE: Keep in this order.
case hotHumid
case marine
case moist
case dry
case marine
public var zoneIdentifiers: [String] {
switch self {
@@ -26,6 +28,6 @@ public enum ClimateZone: String, Codable, Equatable, Sendable {
}
public var label: String {
return "\(rawValue.capitalized) (\(zoneIdentifiers.joined(separator: ",")))"
return "\(self == .hotHumid ? "Hot Humid" : rawValue.capitalized) (\(zoneIdentifiers.joined(separator: ", ")))"
}
}

View File

@@ -4,20 +4,25 @@ public enum FilterPressureDrop {
Calculate filter pressure drop and sizing based on system requirements.
"""
public enum Mode: String, CaseIterable, Codable, Equatable, Sendable {
case basic
case fanLaw
}
public enum Request: Codable, Equatable, Sendable {
case basic(Basic)
case fanLaw(FanLaw)
public struct Basic: Codable, Equatable, Sendable {
let systemSize: HVACSystemSize
let systemSize: Double
let climateZone: ClimateZone
let filterType: FilterType
let filterWidth: Double
let filterHeight: Double
public init(
systemSize: HVACSystemSize,
systemSize: Double,
climateZone: ClimateZone,
filterType: FilterPressureDrop.FilterType,
filterWidth: Double,
@@ -58,22 +63,30 @@ public enum FilterPressureDrop {
}
}
public enum Result: Codable, Equatable, Sendable {
public enum Response: Codable, Equatable, Sendable {
case basic(Basic)
case fanLaw(FanLaw)
public struct Basic: Codable, Equatable, Sendable {
let filterArea: Double
let feetPerMinute: Double
let initialPressureDrop: Double
let maxPressureDrop: Double
public let filterArea: Double
public let feetPerMinute: Double
public let initialPressureDrop: Double
public let maxPressureDrop: Double
public let warnings: [String]
public init(filterArea: Double, feetPerMinute: Double, initialPressureDrop: Double, maxPressureDrop: Double) {
public init(
filterArea: Double,
feetPerMinute: Double,
initialPressureDrop: Double,
maxPressureDrop: Double,
warnings: [String]
) {
self.filterArea = filterArea
self.feetPerMinute = feetPerMinute
self.initialPressureDrop = initialPressureDrop
self.maxPressureDrop = maxPressureDrop
self.warnings = warnings
}
}
@@ -98,7 +111,7 @@ public enum FilterPressureDrop {
}
}
public enum FilterType: String, Codable, Equatable, Sendable {
public enum FilterType: String, CaseIterable, Codable, Equatable, Sendable {
case fiberglass
case pleatedBasic
case pleatedBetter
@@ -114,3 +127,34 @@ public enum FilterPressureDrop {
}
}
}
#if DEBUG
public extension FilterPressureDrop.Response {
static func mock(mode: FilterPressureDrop.Mode) -> Self {
switch mode {
case .basic:
return .basic(.init(
filterArea: 3.47,
feetPerMinute: 230,
initialPressureDrop: 0.2,
maxPressureDrop: 0.15,
warnings: [
"""
Intial pressure drop is more than 50% of maximum allowable.
Consider using a larger filter or different type with lower intial pressure drop.
"""
]
))
case .fanLaw:
return .fanLaw(.init(
predictedPressureDrop: 0.127,
velocityRatio: 1.13,
pressureRatio: 1.27,
faceVelocity: 259
))
}
}
}
#endif

View File

@@ -1,4 +1,4 @@
public enum HVACSystemSize: Double, Codable, Equatable, Sendable {
public enum HVACSystemSize: Double, CaseIterable, Codable, Equatable, Sendable {
case one = 1
case oneAndAHalf = 1.5
case two = 2

View File

@@ -13,6 +13,10 @@ public enum SiteRoute: Equatable, Sendable {
Route(.case(Self.api)) {
Api.router
}
Route(.case(Self.health)) {
Path { "health" }
Method.get
}
Route(.case(Self.view)) {
View.router
}
@@ -26,6 +30,7 @@ public extension SiteRoute {
case calculateAtticVentilation(AtticVentilation.Request)
case calculateCapacitor(Capacitor.Request)
case calculateDehumidifierSize(DehumidifierSize.Request)
case calculateFilterPressureDrop(FilterPressureDrop.Request)
case calculateHVACSystemPerformance(HVACSystemPerformance.Request)
case calculateMoldRisk(MoldRisk.Request)
case calculateRoomPressure(RoomPressure.Request)
@@ -53,6 +58,16 @@ public extension SiteRoute {
Method.post
Body(.json(DehumidifierSize.Request.self))
}
Route(.case(Self.calculateFilterPressureDrop)) {
Path { "api"; "v1"; "calculateFilterPressureDrop" }
Method.post
OneOf {
Body(.json(FilterPressureDrop.Request.Basic.self))
.map(.case(FilterPressureDrop.Request.basic))
Body(.json(FilterPressureDrop.Request.FanLaw.self))
.map(.case(FilterPressureDrop.Request.fanLaw))
}
}
Route(.case(Self.calculateHVACSystemPerformance)) {
Path { "api"; "v1"; "calculateHVACSystemPerformance" }
Method.post
@@ -84,6 +99,7 @@ public extension SiteRoute {
case atticVentilation(AtticVentilation)
case capacitor(Capacitor)
case dehumidifierSize(DehumidifierSize)
case filterPressureDrop(FilterPressureDrop)
case hvacSystemPerformance(HVACSystemPerformance)
case moldRisk(MoldRisk)
case roomPressure(RoomPressure)
@@ -101,6 +117,9 @@ public extension SiteRoute {
Route(.case(Self.dehumidifierSize)) {
DehumidifierSize.router
}
Route(.case(Self.filterPressureDrop)) {
FilterPressureDrop.router
}
Route(.case(Self.hvacSystemPerformance)) {
HVACSystemPerformance.router
}
@@ -213,6 +232,53 @@ public extension SiteRoute {
}
}
public enum FilterPressureDrop: Equatable, Sendable {
case index(mode: Routes.FilterPressureDrop.Mode? = nil)
case submit(Routes.FilterPressureDrop.Request)
public static let index = Self.index()
static let rootPath = "filter-pressure-drop"
public static let router = OneOf {
Route(.case(Self.index)) {
Path { rootPath }
Method.get
Query {
Optionally { Field("mode") { Routes.FilterPressureDrop.Mode.parser() } }
}
}
Route(.case(Self.submit)) {
Path { rootPath }
Method.post
Body {
OneOf {
FormData {
Field("systemSize") { Double.parser() }
Field("climateZone") { ClimateZone.parser() }
Field("filterType") { Routes.FilterPressureDrop.FilterType.parser() }
Field("filterWidth") { Double.parser() }
Field("filterHeight") { Double.parser() }
}
.map(.memberwise(Routes.FilterPressureDrop.Request.Basic.init))
.map(.case(Routes.FilterPressureDrop.Request.basic))
FormData {
Field("filterWidth") { Double.parser() }
Field("filterHeight") { Double.parser() }
Field("filterDepth") { Double.parser() }
Field("ratedAirflow") { Double.parser() }
Field("ratedPressureDrop") { Double.parser() }
Field("designAirflow") { Double.parser() }
}
.map(.memberwise(Routes.FilterPressureDrop.Request.FanLaw.init))
.map(.case(Routes.FilterPressureDrop.Request.fanLaw))
}
}
}
}
}
public enum HVACSystemPerformance: Equatable, Sendable {
case index
case submit(Routes.HVACSystemPerformance.Request)

View File

@@ -1,4 +1,5 @@
import Elementary
import Routes
public struct PrimaryButton: HTML, Sendable {
let label: String
@@ -75,3 +76,70 @@ public struct ResetButton: HTML, Sendable {
}
}
}
public struct Toggle: HTML {
let isOn: Bool
// Left hand side / 'on' label.
let onLabel: String
// Applied to the rhs when the toggle is consider 'off'.
let onAttributes: [HTMLAttribute<HTMLTag.button>]
// Right hand side / 'off' label.
let offLabel: String
// Applied to the left hand side when the toggle is consider 'on'.
let offAttributes: [HTMLAttribute<HTMLTag.button>]
public init(
isOn: Bool,
onLabel: String,
onAttributes: [HTMLAttribute<HTMLTag.button>],
offLabel: String,
offAttributes: [HTMLAttribute<HTMLTag.button>]
) {
self.isOn = isOn
self.onLabel = onLabel
self.onAttributes = onAttributes
self.offLabel = offLabel
self.offAttributes = offAttributes
}
public var content: some HTML<HTMLTag.div> {
div(.class("flex items-center gap-x-0")) {
switch isOn {
case true:
SecondaryButton(label: onLabel)
.attributes(.class("rounded-s-lg"), .disabled)
PrimaryButton(label: offLabel)
.attributes(contentsOf: offAttributes + [.class("rounded-e-lg")])
case false:
PrimaryButton(label: onLabel)
.attributes(contentsOf: onAttributes + [.class("rounded-s-lg")])
SecondaryButton(label: offLabel)
.attributes(.class("rounded-e-lg"), .disabled)
}
}
}
}
public extension Array where Element == HTMLAttribute<HTMLTag.button> {
static func hxDefaults(
_ attributes: HTMLAttribute<HTMLTag.button>...
) -> Self {
[
.hx.target("#content"),
.hx.pushURL(true)
] + attributes
}
static func hxDefaults(
get route: SiteRoute.View
) -> Self {
.hxDefaults(.hx.get(route: route))
}
}

View File

@@ -50,7 +50,7 @@ public struct Input: HTML, Sendable {
input(
.id(id), .placeholder(placeholder), .name(name ?? id),
.class("""
w-full px-4 py-2 border rounded-md
w-full px-4 py-2 border rounded-md min-h-11
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
placeholder-shown:!border-gray-400
invalid:border-red-500 out-of-range:border-red-500
@@ -84,3 +84,57 @@ public struct InputLabel<InputLabel: HTML>: HTML {
}
extension InputLabel: Sendable where InputLabel: Sendable {}
public struct Select<T>: HTML {
let id: String
let name: String?
let values: [T]
let label: @Sendable (T) -> String
let value: @Sendable (T) -> String
public init(
id: String,
name: String? = nil,
values: [T],
label: @escaping @Sendable (T) -> String,
value: @escaping @Sendable (T) -> String
) {
self.id = id
self.name = name
self.values = values
self.label = label
self.value = value
}
public var content: some HTML<HTMLTag.select> {
select(
.id(id),
.name(name ?? id),
.class("w-full rounded-md border px-4 py-2 min-h-11")
) {
for value in values {
option(.value(self.value(value))) { label(value) }
}
}
}
}
extension Select: Sendable where T: Sendable {}
public extension Select where T: CaseIterable, T: RawRepresentable, T.RawValue: CustomStringConvertible {
init(
for type: T.Type,
id: String,
name: String? = nil,
label: @escaping @Sendable (T) -> String
) {
self.init(
id: id,
name: name,
values: Array(T.allCases),
label: label,
value: { $0.rawValue.description }
)
}
}

View File

@@ -10,11 +10,16 @@ public struct Note: HTML, Sendable {
}
public var content: some HTML {
div(.class("mt-8 p-4 bg-gray-100 dark:bg-gray-700 rounded-md shadow-md border border-blue-500")) {
p(.class("text-sm text-blue-500")) {
span(.class("font-extrabold pe-2")) { label }
text
}
div(
.class(
"""
mt-8 p-4 bg-gray-100 dark:bg-gray-700 rounded-md shadow-md
border border-blue-500 text-blue-500 text-sm
"""
)
) {
p(.class("font-extrabold mb-3")) { label }
p(.class("px-6")) { text }
}
}
}

View File

@@ -0,0 +1,34 @@
import Elementary
public struct Row<Body: HTML>: HTML {
let body: Body
public init(
@HTMLBuilder body: () -> Body
) {
self.body = body()
}
public var content: some HTML<HTMLTag.div> {
div(.class("flex justify-between")) {
body
}
}
}
public extension Row
where Body == _HTMLTuple2<HTMLElement<HTMLTag.span, HTMLText>, HTMLElement<HTMLTag.span, HTMLText>>
{
init(
label: String,
value: String
) {
self.init {
span(.class("font-bold")) { label }
span { value }
}
}
}
extension Row: Sendable where Body: Sendable {}

View File

@@ -0,0 +1,25 @@
import Elementary
public struct VerticalGroup: HTML, Sendable {
let label: String
let value: String
let valueLabel: String?
public init(label: String, value: String, valueLabel: String? = nil) {
self.label = label
self.value = value
self.valueLabel = valueLabel
}
public var content: some HTML<HTMLTag.div> {
div(.class("grid grid-cols-1 justify-items-center")) {
p(.class("font-medium")) { label }
h3(.class("text-3xl font-extrabold")) {
value
if let valueLabel {
span(.class("text-lg ms-2")) { valueLabel }
}
}
}
}
}

View File

@@ -43,6 +43,12 @@ struct HomePage: HTML, Sendable {
svg: .zap,
route: .capacitor(.index)
)
group(
label: "Filter Pressure Drop",
description: FilterPressureDrop.description,
svg: .funnel,
route: .filterPressureDrop(.index)
)
}
}
@@ -53,8 +59,7 @@ struct HomePage: HTML, Sendable {
route: SiteRoute.View
) -> some HTML {
button(
.hx.get(route: route),
.hx.target("#content"),
.hx.get(route: route), .hx.target("#content"), .hx.pushURL(true),
.class("""
w-full rounded-xl shadow-lg border border-blue-600 justify-items-start
hover:bg-blue-600 hover:border-yellow-300 hover:text-yellow-300 transition-colors

View File

@@ -100,6 +100,16 @@ extension ViewController: DependencyKey {
return DehumidifierSizeResult(response: response)
}
case let .filterPressureDrop(route):
switch route {
case let .index(mode: mode):
// FIX: remove response.
return request.respond(FilterPressureDropForm(mode: mode, response: .mock(mode: mode ?? .basic)))
case .submit:
// FIX: implement.
fatalError()
}
case let .hvacSystemPerformance(route):
switch route {
case .index:

View File

@@ -143,6 +143,7 @@ struct AtticVentilationResponse: HTML, Sendable {
}
}
// TODO: Use Styleguid.VerticalGroup
private func group(_ label: String, _ value: String) -> some HTML<HTMLTag.div> {
div(.class("justify-self-center")) {
div(.class("grid grid-cols-1 justify-items-center")) {

View File

@@ -17,42 +17,16 @@ struct CapacitorForm: HTML, Sendable {
div(.class("flex flex-wrap justify-between")) {
FormHeader(label: "Capacitor Calculator - \(mode.rawValue.capitalized) Capacitor", svg: .zap)
// Mode toggle / buttons.
div(.class("flex items-center gap-x-0")) {
switch mode {
case .test:
SecondaryButton(label: "Test Capacitor")
.attributes(.class("rounded-s-lg"))
.attributes(.disabled, when: mode == .test)
.attributes(
.hx.get(route: .capacitor(.index(mode: .test))),
.hx.target("#content"),
when: mode == .size
// Mode toggle / buttons.
Toggle(
isOn: mode == .test,
onLabel: "Test Capacitor",
onAttributes: .hxDefaults(get: .capacitor(.index(mode: .test))),
offLabel: "Size Capacitor",
offAttributes: .hxDefaults(get: .capacitor(.index(mode: .size)))
)
PrimaryButton(label: "Size Capacitor")
.attributes(.class("rounded-e-lg"))
.attributes(
.hx.get(route: .capacitor(.index(mode: .size))),
.hx.target("#content"),
when: mode == .test
)
case .size:
PrimaryButton(label: "Test Capacitor")
.attributes(.class("rounded-s-lg"))
.attributes(
.hx.get(route: .capacitor(.index(mode: .test))),
.hx.target("#content"),
when: mode == .size
)
SecondaryButton(label: "Size Capacitor")
.attributes(.class("rounded-e-lg"))
.attributes(.disabled, when: mode == .size)
.attributes(
.hx.get(route: .capacitor(.index(mode: .size))),
.hx.target("#content"),
when: mode == .test
)
}
}
.attributes(.class("mb-6"))
}
Form(mode: mode).attributes(.class("mt-6"))

View File

@@ -0,0 +1,249 @@
import Elementary
import ElementaryHTMX
import Routes
import Styleguide
struct FilterPressureDropForm: HTML, Sendable {
let mode: FilterPressureDrop.Mode
let response: FilterPressureDrop.Response?
init(mode: FilterPressureDrop.Mode? = nil, response: FilterPressureDrop.Response? = nil) {
self.mode = mode ?? .basic
self.response = response
}
var content: some HTML {
div(.class("flex flex-wrap justify-between")) {
FormHeader(label: "Filter Pressure Drop - \(mode.label)", svg: .funnel)
Toggle(
isOn: mode == .basic,
onLabel: FilterPressureDrop.Mode.basic.label,
onAttributes: .hxDefaults(get: .filterPressureDrop(.index(mode: .basic))),
offLabel: FilterPressureDrop.Mode.fanLaw.label,
offAttributes: .hxDefaults(get: .filterPressureDrop(.index(mode: .fanLaw)))
)
.attributes(.class("mb-6"))
}
// FIX: Remove when done.
WarningBox("This is under construction and does not work currently.")
form(
.hx.post(route: .filterPressureDrop(.index)),
.hx.target("#result")
) {
div(.class("space-y-6")) {
switch mode {
case .basic:
BasicFormFields()
case .fanLaw:
FanLawFormFields()
}
div {
SubmitButton(label: "Calculate Pressure Drop")
}
}
}
div(.id("result")) {
if let response {
FilterPressureDropResponse(response: response)
}
}
}
private struct BasicFormFields: HTML, Sendable {
var content: some HTML {
div {
div {
h4(.class("text-lg font-bold mb-4")) { "System Parameters" }
div(.class("grid grid-cols-1 lg:grid-cols-3 gap-x-4 space-y-6")) {
LabeledContent(label: "System Size: (Tons)") {
Input(id: "systemSize", placeholder: "Tons")
.attributes(.type(.number), .step("0.5"), .min("1.0"), .autofocus, .required)
}
div {
InputLabel(for: "climateZone") { "Climate Zone" }
Select(for: ClimateZone.self, id: "climateZone") {
$0.label
}
}
div {
InputLabel(for: "filterType") { "Filter Type" }
Select(for: FilterPressureDrop.FilterType.self, id: "filterType") {
$0.label
}
}
}
}
div(.class("mt-6 lg:mt-0")) {
h4(.class("text-lg font-bold mb-4")) { "Filter Parameters" }
div(.class("grid grid-cols-1 lg:grid-cols-2 gap-x-4 space-y-6")) {
LabeledContent(label: "Width: (in)") {
Input(id: "filterWidth", placeholder: "Width")
.attributes(.type(.number), .step("0.1"), .min("0.1"), .required)
}
LabeledContent(label: "Height: (in)") {
Input(id: "filterHeight", placeholder: "Height")
.attributes(.type(.number), .step("0.1"), .min("0.1"), .required)
}
}
}
}
}
}
private struct FanLawFormFields: HTML, Sendable {
var content: some HTML {
div {
div(.class("mt-6 lg:mt-0")) {
h4(.class("text-lg font-bold mb-4")) { "Filter Parameters" }
div(.class("grid grid-cols-1 lg:grid-cols-3 gap-x-4 space-y-6")) {
LabeledContent(label: "Width: (in)") {
Input(id: "filterWidth", placeholder: "Width")
.attributes(.type(.number), .step("0.1"), .min("0.1"), .required, .autofocus)
}
LabeledContent(label: "Height: (in)") {
Input(id: "filterHeight", placeholder: "Height")
.attributes(.type(.number), .step("0.1"), .min("0.1"), .required)
}
LabeledContent(label: "Depth: (in)") {
Input(id: "filterDepth", placeholder: "Depth")
.attributes(.type(.number), .step("1.0"), .min("1.0"), .required)
}
}
}
div(.class("mt-6 lg:mt-0")) {
h4(.class("text-lg font-bold mb-4")) { "Airflow Parameters" }
div(.class("grid grid-cols-1 lg:grid-cols-3 gap-x-4 space-y-6")) {
LabeledContent(label: "Rated Airflow: (CFM)") {
Input(id: "ratedAirflow", placeholder: "Rated or measured Airflow")
.attributes(.type(.number), .step("0.1"), .min("0.1"), .required)
}
LabeledContent(label: "Filter Pressure Drop: (in. w.c.)") {
Input(id: "ratedPressure", placeholder: "Rated or measured pressure drop")
.attributes(.type(.number), .step("0.1"), .min("0.1"), .required)
}
LabeledContent(label: "Design Airflow: (CFM)") {
Input(id: "designAirflow", placeholder: "Design or target airflow")
.attributes(.type(.number), .step("1.0"), .min("1.0"), .required)
}
}
}
}
}
}
}
struct FilterPressureDropResponse: HTML, Sendable {
let response: FilterPressureDrop.Response
var content: some HTML {
ResultContainer(reset: .filterPressureDrop(.index(mode: response.resetMode))) {
switch response {
case let .basic(response):
BasicView(response: response)
case let .fanLaw(response):
FanLawView(response: response)
}
}
}
private struct BasicView: HTML {
let response: FilterPressureDrop.Response.Basic
var content: some HTML {
div(
.class("""
grid grid-cols-1 lg:grid-cols-2 gap-6 rounded-lg shadow-lg
border border-blue-600 text-blue-600 bg-blue-100 p-6
""")
) {
VerticalGroup(
label: "Filter Face Area",
value: "\(double: response.filterArea, fractionDigits: 2)",
valueLabel: "ft²"
)
VerticalGroup(
label: "Filter Face Velocity",
value: "\(double: response.feetPerMinute, fractionDigits: 1)",
valueLabel: "FPM"
)
VerticalGroup(
label: "Initial Pressure Drop",
value: "\(double: response.initialPressureDrop, fractionDigits: 2)\"",
valueLabel: "w.c."
)
VerticalGroup(
label: "Maximum Allowable Pressure Drop",
value: "\(double: response.maxPressureDrop, fractionDigits: 2)\"",
valueLabel: "w.c."
)
}
WarningBox(warnings: response.warnings)
}
}
private struct FanLawView: HTML {
let response: FilterPressureDrop.Response.FanLaw
var content: some HTML {
div(
.class("""
grid grid-cols-1 lg:grid-cols-2 gap-6 rounded-lg shadow-lg
border border-blue-600 text-blue-600 bg-blue-100 p-6
""")
) {
VerticalGroup(
label: "Filter Face Velocity",
value: "\(double: response.faceVelocity, fractionDigits: 1)",
valueLabel: "FPM"
)
VerticalGroup(
label: "Predicted Pressure Drop",
value: "\(double: response.predictedPressureDrop, fractionDigits: 2)\"",
valueLabel: "w.c."
)
VerticalGroup(
label: "Velocity Ratio",
value: "\(double: response.velocityRatio, fractionDigits: 2)"
)
VerticalGroup(
label: "Pressure Ratio",
value: "\(double: response.pressureRatio, fractionDigits: 2)"
)
}
Note {
"""
Predictions are based on fan laws where pressure drop varies with the square of the airflow ratio.
Results assume similar air properties and filter loading conditions.
"""
}
}
}
}
private extension FilterPressureDrop.Mode {
var label: String {
switch self {
case .basic: return rawValue.capitalized
case .fanLaw: return "Fan Law"
}
}
}
private extension FilterPressureDrop.Response {
var resetMode: FilterPressureDrop.Mode {
switch self {
case .basic: return .basic
case .fanLaw: return .fanLaw
}
}
}

View File

@@ -16,44 +16,14 @@ struct RoomPressureForm: HTML, Sendable {
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
Toggle(
isOn: mode == .knownAirflow,
onLabel: "Known Airflow",
onAttributes: .hxDefaults(get: .roomPressure(.index(mode: .knownAirflow))),
offLabel: "Measured Pressure",
offAttributes: .hxDefaults(get: .roomPressure(.index(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
)
}
}
.attributes(.class("mb-6"))
}
Form(mode: mode)
@@ -156,6 +126,7 @@ struct RoomPressureForm: HTML, Sendable {
var content: some HTML {
InputLabel(for: "preferredGrilleHeight") { "Preferred Grille Height" }
// TODO: Use Styleguide.Select
select(
.id("preferredGrilleHeight"),
.name("preferredGrilleHeight"),