feat: Adds level field to rooms, updates urls to point to public mirror of the project.
Some checks failed
CI / Linux Tests (push) Failing after 15s

This commit is contained in:
2026-02-10 12:07:44 -05:00
parent 980d99e40b
commit dc9f51c04f
28 changed files with 464 additions and 104 deletions

View File

@@ -1,5 +1,5 @@
{ {
"originHash" : "9668a267c19bc66e844dc3e46718c4415359171b587f26c2a592bc347bd5446a", "originHash" : "5bbd172c602e6484b32782f8cb68faca2d5120adc511acdff74e62fec2178c15",
"pins" : [ "pins" : [
{ {
"identity" : "async-http-client", "identity" : "async-http-client",
@@ -456,8 +456,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/m-housh/swift-validations.git", "location" : "https://github.com/m-housh/swift-validations.git",
"state" : { "state" : {
"revision" : "95ea5d267e37f6cdb9f91c5c8a01e718b9299db6", "revision" : "ae939c146f380ca12d0a04ca1f6b0c4c270fdd5a",
"version" : "0.3.4" "version" : "0.3.5"
} }
}, },
{ {

View File

@@ -36,7 +36,7 @@ let package = Package(
.package(url: "https://github.com/elementary-swift/elementary.git", from: "0.6.0"), .package(url: "https://github.com/elementary-swift/elementary.git", from: "0.6.0"),
.package(url: "https://github.com/elementary-swift/elementary-htmx.git", from: "0.5.0"), .package(url: "https://github.com/elementary-swift/elementary-htmx.git", from: "0.5.0"),
.package(url: "https://github.com/vapor-community/vapor-elementary.git", from: "0.1.0"), .package(url: "https://github.com/vapor-community/vapor-elementary.git", from: "0.1.0"),
.package(url: "https://github.com/m-housh/swift-validations.git", from: "0.1.0"), .package(url: "https://github.com/m-housh/swift-validations.git", from: "0.3.5"),
], ],
targets: [ targets: [
.executableTarget( .executableTarget(
@@ -168,6 +168,7 @@ let package = Package(
.product(name: "CasePaths", package: "swift-case-paths"), .product(name: "CasePaths", package: "swift-case-paths"),
.product(name: "Dependencies", package: "swift-dependencies"), .product(name: "Dependencies", package: "swift-dependencies"),
.product(name: "Fluent", package: "fluent"), .product(name: "Fluent", package: "fluent"),
.product(name: "Tagged", package: "swift-tagged"),
.product(name: "URLRouting", package: "swift-url-routing"), .product(name: "URLRouting", package: "swift-url-routing"),
] ]
), ),

View File

@@ -6920,6 +6920,12 @@
} }
} }
} }
.grid-flow-col {
grid-auto-flow: column;
}
.grid-flow-row {
grid-auto-flow: row;
}
.grid-cols-1 { .grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr)); grid-template-columns: repeat(1, minmax(0, 1fr));
} }
@@ -8753,6 +8759,9 @@
color: var(--color-warning); color: var(--color-warning);
} }
} }
.text-accent {
color: var(--color-accent);
}
.text-base-content { .text-base-content {
color: var(--color-base-content); color: var(--color-base-content);
} }
@@ -9732,6 +9741,31 @@
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
} }
} }
.md\:justify-between {
@media (width >= 48rem) {
justify-content: space-between;
}
}
.md\:place-self-center {
@media (width >= 48rem) {
place-self: center;
}
}
.md\:place-self-end {
@media (width >= 48rem) {
place-self: end;
}
}
.md\:place-self-start {
@media (width >= 48rem) {
place-self: start;
}
}
.md\:justify-self-end {
@media (width >= 48rem) {
justify-self: flex-end;
}
}
.lg\:drawer-open { .lg\:drawer-open {
@media (width >= 64rem) { @media (width >= 64rem) {
@layer daisyui.l1.l2.l3 { @layer daisyui.l1.l2.l3 {

View File

@@ -38,6 +38,11 @@ struct RoomCreateParser: ParserPrinter {
ParsePrint { ParsePrint {
Prefix { $0 != UInt8(ascii: ",") }.map(.string) Prefix { $0 != UInt8(ascii: ",") }.map(.string)
",".utf8 ",".utf8
Optionally {
Int.parser()
.map(.memberwise(Room.Level.init(rawValue:)))
}
",".utf8
Double.parser() Double.parser()
",".utf8 ",".utf8
Optionally { Optionally {

View File

@@ -28,6 +28,7 @@ extension DatabaseClient.Rooms: TestDependencyKey {
$0.delegatedToName != nil && $0.delegatedToName != "" $0.delegatedToName != nil && $0.delegatedToName != ""
}) })
// Filter out the rest of the rooms that don't delegate their airflow / loads.
let initialRooms = rows.filter({ let initialRooms = rows.filter({
$0.delegatedToName == nil || $0.delegatedToName == "" $0.delegatedToName == nil || $0.delegatedToName == ""
}) })
@@ -54,6 +55,7 @@ extension DatabaseClient.Rooms: TestDependencyKey {
array.append( array.append(
Room.Create.init( Room.Create.init(
name: row.name, name: row.name,
level: row.level,
heatingLoad: row.heatingLoad, heatingLoad: row.heatingLoad,
coolingTotal: row.coolingTotal, coolingTotal: row.coolingTotal,
coolingSensible: row.coolingSensible, coolingSensible: row.coolingSensible,
@@ -134,6 +136,7 @@ extension Room.CSV.Row {
assert(delegatedToName == nil || delegatedToName == "") assert(delegatedToName == nil || delegatedToName == "")
return .init( return .init(
name: name, name: name,
level: level,
heatingLoad: heatingLoad, heatingLoad: heatingLoad,
coolingTotal: coolingTotal, coolingTotal: coolingTotal,
coolingSensible: coolingSensible, coolingSensible: coolingSensible,
@@ -170,6 +173,7 @@ extension Room.Create {
return .init( return .init(
name: name, name: name,
level: level?.rawValue,
heatingLoad: heatingLoad, heatingLoad: heatingLoad,
coolingLoad: coolingLoad, coolingLoad: coolingLoad,
registerCount: registerCount, registerCount: registerCount,
@@ -187,6 +191,7 @@ extension Room {
try await database.schema(RoomModel.schema) try await database.schema(RoomModel.schema)
.id() .id()
.field("name", .string, .required) .field("name", .string, .required)
.field("level", .int8)
.field("heatingLoad", .double, .required) .field("heatingLoad", .double, .required)
.field("coolingLoad", .dictionary, .required) .field("coolingLoad", .dictionary, .required)
.field("registerCount", .int8, .required) .field("registerCount", .int8, .required)
@@ -217,6 +222,9 @@ final class RoomModel: Model, @unchecked Sendable, Validatable {
@Field(key: "name") @Field(key: "name")
var name: String var name: String
@Field(key: "level")
var level: Int?
@Field(key: "heatingLoad") @Field(key: "heatingLoad")
var heatingLoad: Double var heatingLoad: Double
@@ -246,6 +254,7 @@ final class RoomModel: Model, @unchecked Sendable, Validatable {
init( init(
id: UUID? = nil, id: UUID? = nil,
name: String, name: String,
level: Int? = nil,
heatingLoad: Double, heatingLoad: Double,
coolingLoad: Room.CoolingLoad, coolingLoad: Room.CoolingLoad,
registerCount: Int, registerCount: Int,
@@ -257,6 +266,7 @@ final class RoomModel: Model, @unchecked Sendable, Validatable {
) { ) {
self.id = id self.id = id
self.name = name self.name = name
self.level = level
self.heatingLoad = heatingLoad self.heatingLoad = heatingLoad
self.coolingLoad = coolingLoad self.coolingLoad = coolingLoad
self.registerCount = registerCount self.registerCount = registerCount
@@ -272,6 +282,7 @@ final class RoomModel: Model, @unchecked Sendable, Validatable {
id: requireID(), id: requireID(),
projectID: $project.id, projectID: $project.id,
name: name, name: name,
level: level.map(Room.Level.init(rawValue:)),
heatingLoad: heatingLoad, heatingLoad: heatingLoad,
coolingLoad: coolingLoad, coolingLoad: coolingLoad,
registerCount: registerCount, registerCount: registerCount,
@@ -287,6 +298,9 @@ final class RoomModel: Model, @unchecked Sendable, Validatable {
if let name = updates.name, name != self.name { if let name = updates.name, name != self.name {
self.name = name self.name = name
} }
if let level = updates.level?.rawValue, level != self.level {
self.level = level
}
if let heatingLoad = updates.heatingLoad, heatingLoad != self.heatingLoad { if let heatingLoad = updates.heatingLoad, heatingLoad != self.heatingLoad {
self.heatingLoad = heatingLoad self.heatingLoad = heatingLoad
} }

View File

@@ -1,5 +1,6 @@
import Dependencies import Dependencies
import Foundation import Foundation
import Tagged
/// Represents a room in a project. /// Represents a room in a project.
/// ///
@@ -17,6 +18,9 @@ public struct Room: Codable, Equatable, Identifiable, Sendable {
/// A unique name for the room in the project. /// A unique name for the room in the project.
public let name: String public let name: String
/// The level of the home the room is on.
public let level: Level?
/// The heating load required for the room (from Manual-J). /// The heating load required for the room (from Manual-J).
public let heatingLoad: Double public let heatingLoad: Double
@@ -45,6 +49,7 @@ public struct Room: Codable, Equatable, Identifiable, Sendable {
id: UUID, id: UUID,
projectID: Project.ID, projectID: Project.ID,
name: String, name: String,
level: Level? = nil,
heatingLoad: Double, heatingLoad: Double,
coolingLoad: CoolingLoad, coolingLoad: CoolingLoad,
registerCount: Int = 1, registerCount: Int = 1,
@@ -56,6 +61,7 @@ public struct Room: Codable, Equatable, Identifiable, Sendable {
self.id = id self.id = id
self.projectID = projectID self.projectID = projectID
self.name = name self.name = name
self.level = level
self.heatingLoad = heatingLoad self.heatingLoad = heatingLoad
self.coolingLoad = coolingLoad self.coolingLoad = coolingLoad
self.registerCount = registerCount self.registerCount = registerCount
@@ -98,6 +104,11 @@ public struct Room: Codable, Equatable, Identifiable, Sendable {
} }
} }
public enum LevelTag {}
public typealias Level = Tagged<LevelTag, Int>
} }
extension Room { extension Room {
@@ -106,6 +117,9 @@ extension Room {
/// A unique name for the room in the project. /// A unique name for the room in the project.
public let name: String public let name: String
/// An optional level of the home the room is on.
public let level: Room.Level?
/// The heating load required for the room (from Manual-J). /// The heating load required for the room (from Manual-J).
public let heatingLoad: Double public let heatingLoad: Double
@@ -127,6 +141,7 @@ extension Room {
public init( public init(
name: String, name: String,
level: Room.Level? = nil,
heatingLoad: Double, heatingLoad: Double,
coolingTotal: Double? = nil, coolingTotal: Double? = nil,
coolingSensible: Double? = nil, coolingSensible: Double? = nil,
@@ -134,6 +149,7 @@ extension Room {
delegatedTo: Room.ID? = nil delegatedTo: Room.ID? = nil
) { ) {
self.name = name self.name = name
self.level = level
self.heatingLoad = heatingLoad self.heatingLoad = heatingLoad
self.coolingTotal = coolingTotal self.coolingTotal = coolingTotal
self.coolingSensible = coolingSensible self.coolingSensible = coolingSensible
@@ -160,6 +176,9 @@ extension Room {
/// A unique name for the room in the project. /// A unique name for the room in the project.
public let name: String public let name: String
/// An optional level of the home the room is on.
public let level: Room.Level?
/// The heating load required for the room (from Manual-J). /// The heating load required for the room (from Manual-J).
public let heatingLoad: Double public let heatingLoad: Double
@@ -177,6 +196,7 @@ extension Room {
public init( public init(
name: String, name: String,
level: Room.Level? = nil,
heatingLoad: Double, heatingLoad: Double,
coolingTotal: Double? = nil, coolingTotal: Double? = nil,
coolingSensible: Double? = nil, coolingSensible: Double? = nil,
@@ -184,6 +204,7 @@ extension Room {
delegatedToName: String? = nil delegatedToName: String? = nil
) { ) {
self.name = name self.name = name
self.level = level
self.heatingLoad = heatingLoad self.heatingLoad = heatingLoad
self.coolingTotal = coolingTotal self.coolingTotal = coolingTotal
self.coolingSensible = coolingSensible self.coolingSensible = coolingSensible
@@ -226,6 +247,10 @@ extension Room {
public struct Update: Codable, Equatable, Sendable { public struct Update: Codable, Equatable, Sendable {
/// A unique name for the room in the project. /// A unique name for the room in the project.
public let name: String? public let name: String?
/// An optional level of the home the room is on.
public let level: Room.Level?
/// The heating load required for the room (from Manual-J). /// The heating load required for the room (from Manual-J).
public let heatingLoad: Double? public let heatingLoad: Double?
/// The total cooling load required for the room (from Manual-J). /// The total cooling load required for the room (from Manual-J).
@@ -246,12 +271,14 @@ extension Room {
public init( public init(
name: String? = nil, name: String? = nil,
level: Room.Level? = nil,
heatingLoad: Double? = nil, heatingLoad: Double? = nil,
coolingTotal: Double? = nil, coolingTotal: Double? = nil,
coolingSensible: Double? = nil, coolingSensible: Double? = nil,
registerCount: Int? = nil registerCount: Int? = nil
) { ) {
self.name = name self.name = name
self.level = level
self.heatingLoad = heatingLoad self.heatingLoad = heatingLoad
self.coolingTotal = coolingTotal self.coolingTotal = coolingTotal
self.coolingSensible = coolingSensible self.coolingSensible = coolingSensible
@@ -263,6 +290,7 @@ extension Room {
rectangularSizes: [RectangularSize] rectangularSizes: [RectangularSize]
) { ) {
self.name = nil self.name = nil
self.level = nil
self.heatingLoad = nil self.heatingLoad = nil
self.coolingTotal = nil self.coolingTotal = nil
self.coolingSensible = nil self.coolingSensible = nil
@@ -304,6 +332,16 @@ public struct CoolingLoadError: Error, Equatable, Sendable {
} }
} }
extension Room.Level {
/// The label for the level, i.e. 'Basement' or 'Level-1', etc.
public var label: String {
if rawValue <= 0 {
return "Basement"
}
return "Level-\(rawValue)"
}
}
#if DEBUG #if DEBUG
extension Room { extension Room {

View File

@@ -216,12 +216,6 @@ extension SiteRoute.View.ProjectRoute {
} }
Method.post Method.post
Body().map(.memberwise(Room.CSV.init)) Body().map(.memberwise(Room.CSV.init))
// Body {
// FormData {
//
// }
// .map(.memberwise(Room.CSV.init))
// }
} }
Route(.case(Self.delete)) { Route(.case(Self.delete)) {
Path { Path {
@@ -242,6 +236,12 @@ extension SiteRoute.View.ProjectRoute {
Body { Body {
FormData { FormData {
Field("name", .string) Field("name", .string)
Optionally {
Field("level") {
Int.parser()
}
.map(.memberwise(Room.Level.init(rawValue:)))
}
Field("heatingLoad") { Double.parser() } Field("heatingLoad") { Double.parser() }
Optionally { Optionally {
Field("coolingTotal") { Double.parser() } Field("coolingTotal") { Double.parser() }
@@ -268,6 +268,12 @@ extension SiteRoute.View.ProjectRoute {
Optionally { Optionally {
Field("name", .string) Field("name", .string)
} }
Optionally {
Field("level") {
Int.parser()
}
.map(.memberwise(Room.Level.init(rawValue:)))
}
Optionally { Optionally {
Field("heatingLoad") { Double.parser() } Field("heatingLoad") { Double.parser() }
} }

View File

@@ -1,5 +1,6 @@
import Elementary import Elementary
import Foundation import Foundation
import Validations
public struct ResultView<ValueView, ErrorView>: HTML where ValueView: HTML, ErrorView: HTML { public struct ResultView<ValueView, ErrorView>: HTML where ValueView: HTML, ErrorView: HTML {
@@ -69,7 +70,11 @@ public struct ErrorView: HTML, Sendable {
div { div {
h1(.class("text-xl font-bold text-error")) { "Oops: Error" } h1(.class("text-xl font-bold text-error")) { "Oops: Error" }
p { p {
"\(error.localizedDescription)" if let validationError = (error as? ValidationError) {
"\(validationError.debugDescription)"
} else {
"\(error.localizedDescription)"
}
} }
} }
} }

View File

@@ -49,7 +49,7 @@ struct HomeView: HTML, Sendable {
header header
a( a(
.class("btn btn-ghost text-md text-primary font-bold italic"), .class("btn btn-ghost text-md text-primary font-bold italic"),
.href("https://git.housh.dev/michael/swift-duct-calc"), .href("https://github.com/m-housh/swift-duct-calc"),
.target(.blank) .target(.blank)
) { ) {
"Open source residential duct design program" "Open source residential duct design program"

View File

@@ -94,22 +94,36 @@ public struct MainPage<Inner: HTML>: SendableHTMLDocument where Inner: Sendable
footer( footer(
.class( .class(
""" """
footer sm:footer-horizontal footer-center footer footer-horizontal footer-center
bg-base-300 text-base-content p-4 bg-base-300 text-base-content p-4
""" """
) )
) { ) {
aside { aside(
p { .class("grid-flow-row items-center")
"Copyright © \(Date().description.prefix(4)) - All rights reserved by Michael Housh" ) {
div(.class("flex mx-auto")) {
a(
.class("btn btn-ghost"),
.href("mailto:support@ductcalc.pro")
) {
SVG(.email)
span { "support@ductcalc.pro" }
}
} }
a( a(
.class("btn btn-ghost"), .class("btn btn-ghost mx-auto"),
.href("https://git.housh.dev/michael/swift-duct-calc/src/branch/main/LICENSE"), .href("https://github.com/m-housh/swift-duct-calc/src/branch/main/LICENSE"),
.target(.blank) .target(.blank)
) { ) {
"Openly licensed via CC-BY-NC-SA 4.0" "Openly licensed via CC-BY-NC-SA 4.0"
} }
p(.class("")) {
"Copyright © \(Date().description.prefix(4)) - All rights reserved by Michael Housh"
}
} }
} }
} }

View File

@@ -34,12 +34,12 @@ struct Navbar: HTML, Sendable {
label( label(
.for("my-drawer-1"), .for("my-drawer-1"),
.class("size-7"), .class("size-7"),
.init(name: "aria-label", value: "open sidebar") .init(name: "aria-label", value: "open / close sidebar")
) { ) {
SVG(.sidebarToggle) SVG(.sidebarToggle)
} }
.navButton() .navButton()
.tooltip("Open sidebar", position: .right) .tooltip("Open / close sidebar", position: .right)
} }
a( a(

View File

@@ -68,6 +68,21 @@ struct RoomForm: HTML, Sendable {
.value(room?.name) .value(room?.name)
) )
LabeledInput(
"Level",
.name("level"),
.type(.number),
.placeholder("1 (Optional)"),
.value(room?.level?.rawValue),
.min("-1"),
.step("1")
)
div(.class("text-sm italic -mt-2")) {
span(.class("text-primary")) {
"Use -1 or 0 for a basement"
}
}
LabeledInput( LabeledInput(
"Heating Load", "Heating Load",
.name("heatingLoad"), .name("heatingLoad"),
@@ -78,8 +93,6 @@ struct RoomForm: HTML, Sendable {
.value(room?.heatingLoad) .value(room?.heatingLoad)
) )
// TODO: Add description that only one is required (cooling total or sensible)
LabeledInput( LabeledInput(
"Cooling Total", "Cooling Total",
.name("coolingTotal"), .name("coolingTotal"),
@@ -97,6 +110,14 @@ struct RoomForm: HTML, Sendable {
.min("0"), .min("0"),
.value(room?.coolingLoad.sensible) .value(room?.coolingLoad.sensible)
) )
div(.class("text-primary text-sm italic -mt-2")) {
p {
"Should enter at least one of the cooling loads."
}
p {
"Both are also acceptable."
}
}
LabeledInput( LabeledInput(
"Registers", "Registers",

View File

@@ -15,6 +15,14 @@ struct RoomsView: HTML, Sendable {
.appendingPath("csv") .appendingPath("csv")
} }
// Sort the rooms based on level, they should already be sorted by name,
// so this puts lower level rooms towards the top in alphabetical order.
//
// If rooms do not have a level we shove those all the way to the bottom.
private var sortedRooms: [Room] {
rooms.sorted { ($0.level?.rawValue ?? 20) < ($1.level?.rawValue ?? 20) }
}
var body: some HTML { var body: some HTML {
div(.class("flex w-full flex-col")) { div(.class("flex w-full flex-col")) {
PageTitleRow { PageTitleRow {
@@ -133,7 +141,7 @@ struct RoomsView: HTML, Sendable {
} }
} }
tbody { tbody {
for room in rooms { for room in sortedRooms {
RoomRow(room: room, shr: sensibleHeatRatio, rooms: rooms) RoomRow(room: room, shr: sensibleHeatRatio, rooms: rooms)
} }
} }
@@ -166,7 +174,13 @@ struct RoomsView: HTML, Sendable {
public var body: some HTML { public var body: some HTML {
tr(.id("roomRow_\(room.id.idString)")) { tr(.id("roomRow_\(room.id.idString)")) {
td { room.name } td {
if let level = room.level {
"\(level.label) - \(room.name)"
} else {
room.name
}
}
td { td {
div(.class("flex justify-center")) { div(.class("flex justify-center")) {
Number(room.heatingLoad, digits: 0) Number(room.heatingLoad, digits: 0)

View File

@@ -1,5 +1,6 @@
import Elementary import Elementary
import ElementaryHTMX import ElementaryHTMX
import ManualDCore
import Styleguide import Styleguide
struct LoginForm: HTML, Sendable { struct LoginForm: HTML, Sendable {
@@ -12,6 +13,13 @@ struct LoginForm: HTML, Sendable {
self.next = next self.next = next
} }
private var route: SiteRoute.View {
if style == .login {
return .login(.index(next: next))
}
return .signup(.index)
}
var body: some HTML { var body: some HTML {
ModalForm(id: "loginForm", closeButton: false, dismiss: false) { ModalForm(id: "loginForm", closeButton: false, dismiss: false) {
h1(.class("text-2xl font-bold mb-6")) { style.title } h1(.class("text-2xl font-bold mb-6")) { style.title }

View File

@@ -150,6 +150,10 @@ struct UserProfileForm: HTML, Sendable {
.attributes(.class("btn-block")) .attributes(.class("btn-block"))
} }
.attributes(
.hx.pushURL("/projects"),
when: signup == true
)
} }
} }
} }

10
TODO.md
View File

@@ -3,8 +3,8 @@
- [x] Fix theme not working when selected upon signup. - [x] Fix theme not working when selected upon signup.
- [x] Pdf generation - [x] Pdf generation
- [x] Add postgres / mysql support - [x] Add postgres / mysql support
- [x] Opensource / license ?? - [x] Opensource / license
- [x] Figure out domain to host (currently thinking ductcalc.pro) - [x] Figure out domain to host
- [x] Add ability for either sensible or total load while specifying a room load. - [x] Add ability for either sensible or total load while specifying a room load.
- CoolCalc current version specifies the sensible cooling for a room break down, - CoolCalc current version specifies the sensible cooling for a room break down,
and currently we require the total load and calculate sensible based on project and currently we require the total load and calculate sensible based on project
@@ -14,5 +14,9 @@
- They will overlap each other making it difficult to read / decipher which checkbox belongs - They will overlap each other making it difficult to read / decipher which checkbox belongs
to which label. to which label.
- [x] Add select all rooms for trunks, useful for sizing main supply or return trunks. - [x] Add select all rooms for trunks, useful for sizing main supply or return trunks.
- [ ] Add optional level to room model. - [x] Add optional level to room model.
- [ ] Add way to sponsor the project. - [ ] Add way to sponsor the project.
- [x] Need to push url after signup.
- [x] Update urls to point to public mirror of repository
- [x] Add email to footer
- [x] Validation errors, if they occur are vague in ResultView

View File

@@ -11,12 +11,15 @@ struct CSVParsingTests {
let parser = CSVParser.liveValue let parser = CSVParser.liveValue
let input = """ let input = """
Name,Heating Load,Cooling Total,Cooling Sensible,Register Count,Delegated To Name,Level,Heating Load,Cooling Total,Cooling Sensible,Register Count,Delegated To
Bed-1,12345,12345,,2, Bed-1,2,12345,2345,2345,2,
Bed-2,1223,,1123,1, Entry,1,3456,1234,990,1,
Kitchen,1,6789,3456,,2,
Bath-1,1,890,,345,0,Kitchen
""" """
let rooms = try await parser.parseRooms(.init(file: Data(input.utf8))) let rooms = try await parser.parseRooms(.init(file: Data(input.utf8)))
#expect(rooms.count == 2) #expect(rooms.count == 4)
#expect(rooms.first!.level == 2)
} }
} }

View File

@@ -1,5 +1,5 @@
Name,Heating Load,Cooling Total,Cooling Sensible,Register Count,Delegated To Name,Level,Heating Load,Cooling Total,Cooling Sensible,Register Count,Delegated To
Bed-1,2345,1234,1321,1, Bed-1,2,2345,1234,1321,1,
Entry,3456,2345,1234,1, Entry,1,3456,2345,1234,1,
Kitchen,7654,3456,2453,2, Kitchen,1,7654,3456,2453,2,
Bath-1,890,345,,0,Kitchen Bath-1,1,890,345,,0,Kitchen
1 Name Level Heating Load Cooling Total Cooling Sensible Register Count Delegated To
2 Bed-1 2 2345 1234 1321 1
3 Entry 1 3456 2345 1234 1
4 Kitchen 1 7654 3456 2453 2
5 Bath-1 1 890 345 0 Kitchen

View File

@@ -69,11 +69,25 @@ p-6 w-full">
</div> </div>
</main> </main>
<div class="bottom-0 left-0 bg-error"> <div class="bottom-0 left-0 bg-error">
<footer class="footer sm:footer-horizontal footer-center <footer class="footer footer-horizontal footer-center
bg-base-300 text-base-content p-4"> bg-base-300 text-base-content p-4">
<aside> <aside class="grid-flow-row items-center">
<p>Copyright © 2026 - All rights reserved by Michael Housh</p> <div class="flex mx-auto">
Openly licensed via CC-BY-NC-SA 4.0<a class="btn btn-ghost" href="https://git.housh.dev/michael/swift-duct-calc/src/branch/main/LICENSE" target="_blank"></a> <a class="btn btn-ghost" href="mailto:support@ductcalc.pro"> <svg class="h-[1em] opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g
stroke-linejoin="round"
stroke-linecap="round"
stroke-width="2.5"
fill="none"
stroke="currentColor"
>
<rect width="20" height="16" x="2" y="4" rx="2"></rect>
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"></path>
</g>
</svg><span>support@ductcalc.pro</span></a>
</div>
Openly licensed via CC-BY-NC-SA 4.0<a class="btn btn-ghost mx-auto" href="https://github.com/m-housh/swift-duct-calc/src/branch/main/LICENSE" target="_blank"></a>
<p class="">Copyright © 2026 - All rights reserved by Michael Housh</p>
</aside> </aside>
</footer> </footer>
</div> </div>

View File

@@ -53,7 +53,7 @@ text-8xl font-bold my-auto space-2">
</div> </div>
</div> </div>
</div> </div>
Open source residential duct design program<a class="btn btn-ghost text-md text-primary font-bold italic" href="https://git.housh.dev/michael/swift-duct-calc" target="_blank"></a> Open source residential duct design program<a class="btn btn-ghost text-md text-primary font-bold italic" href="https://github.com/m-housh/swift-duct-calc" target="_blank"></a>
<p class="text-3xl py-6">Manual-D™ speed sheet, but on the web!</p> <p class="text-3xl py-6">Manual-D™ speed sheet, but on the web!</p>
<button class="btn btn-xl btn-primary mt-6" hx-get="/signup" hx-target="body" hx-swap="outerHTML">Get Started</button> <button class="btn btn-xl btn-primary mt-6" hx-get="/signup" hx-target="body" hx-swap="outerHTML">Get Started</button>
<p class="text-xs italic my-6"> <p class="text-xs italic my-6">
@@ -100,11 +100,25 @@ text-8xl font-bold my-auto space-2">
</div> </div>
</main> </main>
<div class="bottom-0 left-0 bg-error"> <div class="bottom-0 left-0 bg-error">
<footer class="footer sm:footer-horizontal footer-center <footer class="footer footer-horizontal footer-center
bg-base-300 text-base-content p-4"> bg-base-300 text-base-content p-4">
<aside> <aside class="grid-flow-row items-center">
<p>Copyright © 2026 - All rights reserved by Michael Housh</p> <div class="flex mx-auto">
Openly licensed via CC-BY-NC-SA 4.0<a class="btn btn-ghost" href="https://git.housh.dev/michael/swift-duct-calc/src/branch/main/LICENSE" target="_blank"></a> <a class="btn btn-ghost" href="mailto:support@ductcalc.pro"> <svg class="h-[1em] opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g
stroke-linejoin="round"
stroke-linecap="round"
stroke-width="2.5"
fill="none"
stroke="currentColor"
>
<rect width="20" height="16" x="2" y="4" rx="2"></rect>
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"></path>
</g>
</svg><span>support@ductcalc.pro</span></a>
</div>
Openly licensed via CC-BY-NC-SA 4.0<a class="btn btn-ghost mx-auto" href="https://github.com/m-housh/swift-duct-calc/src/branch/main/LICENSE" target="_blank"></a>
<p class="">Copyright © 2026 - All rights reserved by Michael Housh</p>
</aside> </aside>
</footer> </footer>
</div> </div>

View File

@@ -36,7 +36,7 @@
<div class="drawer-content overflow-auto"> <div class="drawer-content overflow-auto">
<nav class="navbar w-full bg-base-300 text-base-content shadow-sm mb-4"> <nav class="navbar w-full bg-base-300 text-base-content shadow-sm mb-4">
<div class="flex flex-1 space-x-4 items-center"> <div class="flex flex-1 space-x-4 items-center">
<div class="tooltip tooltip-right" data-tip="Open sidebar"><label for="my-drawer-1" class="size-7 btn btn-square btn-ghost hover:bg-neutral hover:text-white" aria-label="open sidebar"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-linejoin="round" stroke-linecap="round" stroke-width="2" fill="none" stroke="currentColor" class="my-1.5 inline-block"><path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path><path d="M9 4v16"></path><path d="M14 10l2 2l-2 2"></path></svg></label></div> <div class="tooltip tooltip-right" data-tip="Open / close sidebar"><label for="my-drawer-1" class="size-7 btn btn-square btn-ghost hover:bg-neutral hover:text-white" aria-label="open / close sidebar"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-linejoin="round" stroke-linecap="round" stroke-width="2" fill="none" stroke="currentColor" class="my-1.5 inline-block"><path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path><path d="M9 4v16"></path><path d="M14 10l2 2l-2 2"></path></svg></label></div>
<div class="tooltip tooltip-right" data-tip="Projects"> <div class="tooltip tooltip-right" data-tip="Projects">
<a class="flex w-fit h-fit text-2xl items-end px-4 py-2 btn btn-square btn-ghost hover:bg-neutral hover:text-white" href="/projects"> <a class="flex w-fit h-fit text-2xl items-end px-4 py-2 btn btn-square btn-ghost hover:bg-neutral hover:text-white" href="/projects">
<img src="/images/mand_logo_sm.webp"> <img src="/images/mand_logo_sm.webp">
@@ -246,11 +246,25 @@ is-drawer-close:justify-center is-drawer-close:mx-auto is-drawer-close:space-y-2
</div> </div>
</main> </main>
<div class="bottom-0 left-0 bg-error"> <div class="bottom-0 left-0 bg-error">
<footer class="footer sm:footer-horizontal footer-center <footer class="footer footer-horizontal footer-center
bg-base-300 text-base-content p-4"> bg-base-300 text-base-content p-4">
<aside> <aside class="grid-flow-row items-center">
<p>Copyright © 2026 - All rights reserved by Michael Housh</p> <div class="flex mx-auto">
Openly licensed via CC-BY-NC-SA 4.0<a class="btn btn-ghost" href="https://git.housh.dev/michael/swift-duct-calc/src/branch/main/LICENSE" target="_blank"></a> <a class="btn btn-ghost" href="mailto:support@ductcalc.pro"> <svg class="h-[1em] opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g
stroke-linejoin="round"
stroke-linecap="round"
stroke-width="2.5"
fill="none"
stroke="currentColor"
>
<rect width="20" height="16" x="2" y="4" rx="2"></rect>
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"></path>
</g>
</svg><span>support@ductcalc.pro</span></a>
</div>
Openly licensed via CC-BY-NC-SA 4.0<a class="btn btn-ghost mx-auto" href="https://github.com/m-housh/swift-duct-calc/src/branch/main/LICENSE" target="_blank"></a>
<p class="">Copyright © 2026 - All rights reserved by Michael Housh</p>
</aside> </aside>
</footer> </footer>
</div> </div>

View File

@@ -36,7 +36,7 @@
<div class="drawer-content overflow-auto"> <div class="drawer-content overflow-auto">
<nav class="navbar w-full bg-base-300 text-base-content shadow-sm mb-4"> <nav class="navbar w-full bg-base-300 text-base-content shadow-sm mb-4">
<div class="flex flex-1 space-x-4 items-center"> <div class="flex flex-1 space-x-4 items-center">
<div class="tooltip tooltip-right" data-tip="Open sidebar"><label for="my-drawer-1" class="size-7 btn btn-square btn-ghost hover:bg-neutral hover:text-white" aria-label="open sidebar"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-linejoin="round" stroke-linecap="round" stroke-width="2" fill="none" stroke="currentColor" class="my-1.5 inline-block"><path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path><path d="M9 4v16"></path><path d="M14 10l2 2l-2 2"></path></svg></label></div> <div class="tooltip tooltip-right" data-tip="Open / close sidebar"><label for="my-drawer-1" class="size-7 btn btn-square btn-ghost hover:bg-neutral hover:text-white" aria-label="open / close sidebar"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-linejoin="round" stroke-linecap="round" stroke-width="2" fill="none" stroke="currentColor" class="my-1.5 inline-block"><path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path><path d="M9 4v16"></path><path d="M14 10l2 2l-2 2"></path></svg></label></div>
<div class="tooltip tooltip-right" data-tip="Projects"> <div class="tooltip tooltip-right" data-tip="Projects">
<a class="flex w-fit h-fit text-2xl items-end px-4 py-2 btn btn-square btn-ghost hover:bg-neutral hover:text-white" href="/projects"> <a class="flex w-fit h-fit text-2xl items-end px-4 py-2 btn btn-square btn-ghost hover:bg-neutral hover:text-white" href="/projects">
<img src="/images/mand_logo_sm.webp"> <img src="/images/mand_logo_sm.webp">
@@ -171,13 +171,20 @@ p-6 w-full">
<input class="hidden" name="id" value="00000000-0000-0000-0000-000000000001"> <input class="hidden" name="id" value="00000000-0000-0000-0000-000000000001">
Name<label class="input w-full"><span class="label"></span> Name<label class="input w-full"><span class="label"></span>
<input name="name" type="text" placeholder="Name" required autofocus value="Bed-1"> <input name="name" type="text" placeholder="Name" required autofocus value="Bed-1">
Heating Load</label><label class="input w-full"><span class="label"></span> Level</label><label class="input w-full"><span class="label"></span>
<input name="level" type="number" placeholder="1 (Optional)" value="" min="-1" step="1"></label>
<div class="text-sm italic -mt-2"><span class="text-primary">Use -1 or 0 for a basement</span></div>
Heating Load<label class="input w-full"><span class="label"></span>
<input name="heatingLoad" type="number" placeholder="1234" required min="0" value="3913.0"> <input name="heatingLoad" type="number" placeholder="1234" required min="0" value="3913.0">
Cooling Total</label><label class="input w-full"><span class="label"></span> Cooling Total</label><label class="input w-full"><span class="label"></span>
<input name="coolingTotal" type="number" placeholder="1234 (Optional)" min="0" value="2472.0"> <input name="coolingTotal" type="number" placeholder="1234 (Optional)" min="0" value="2472.0">
Cooling Sensible</label><label class="input w-full"><span class="label"></span> Cooling Sensible</label><label class="input w-full"><span class="label"></span>
<input name="coolingSensible" type="number" placeholder="1234 (Optional)" min="0" value=""> <input name="coolingSensible" type="number" placeholder="1234 (Optional)" min="0" value=""></label>
Registers</label><label class="input w-full"><span class="label"></span> <div class="text-primary text-sm italic -mt-2">
<p>Should enter at least one of the cooling loads.</p>
<p>Both are also acceptable.</p>
</div>
Registers<label class="input w-full"><span class="label"></span>
<input name="registerCount" type="number" min="1" required value="1" id="registerCount"> <input name="registerCount" type="number" min="1" required value="1" id="registerCount">
Room</label><label class="select w-full"><span class="label"></span> Room</label><label class="select w-full"><span class="label"></span>
<select name="delegatedTo"> <select name="delegatedTo">
@@ -234,13 +241,20 @@ p-6 w-full">
<input class="hidden" name="id" value="00000000-0000-0000-0000-000000000002"> <input class="hidden" name="id" value="00000000-0000-0000-0000-000000000002">
Name<label class="input w-full"><span class="label"></span> Name<label class="input w-full"><span class="label"></span>
<input name="name" type="text" placeholder="Name" required autofocus value="Entry"> <input name="name" type="text" placeholder="Name" required autofocus value="Entry">
Heating Load</label><label class="input w-full"><span class="label"></span> Level</label><label class="input w-full"><span class="label"></span>
<input name="level" type="number" placeholder="1 (Optional)" value="" min="-1" step="1"></label>
<div class="text-sm italic -mt-2"><span class="text-primary">Use -1 or 0 for a basement</span></div>
Heating Load<label class="input w-full"><span class="label"></span>
<input name="heatingLoad" type="number" placeholder="1234" required min="0" value="8284.0"> <input name="heatingLoad" type="number" placeholder="1234" required min="0" value="8284.0">
Cooling Total</label><label class="input w-full"><span class="label"></span> Cooling Total</label><label class="input w-full"><span class="label"></span>
<input name="coolingTotal" type="number" placeholder="1234 (Optional)" min="0" value="2916.0"> <input name="coolingTotal" type="number" placeholder="1234 (Optional)" min="0" value="2916.0">
Cooling Sensible</label><label class="input w-full"><span class="label"></span> Cooling Sensible</label><label class="input w-full"><span class="label"></span>
<input name="coolingSensible" type="number" placeholder="1234 (Optional)" min="0" value=""> <input name="coolingSensible" type="number" placeholder="1234 (Optional)" min="0" value=""></label>
Registers</label><label class="input w-full"><span class="label"></span> <div class="text-primary text-sm italic -mt-2">
<p>Should enter at least one of the cooling loads.</p>
<p>Both are also acceptable.</p>
</div>
Registers<label class="input w-full"><span class="label"></span>
<input name="registerCount" type="number" min="1" required value="2" id="registerCount"> <input name="registerCount" type="number" min="1" required value="2" id="registerCount">
Room</label><label class="select w-full"><span class="label"></span> Room</label><label class="select w-full"><span class="label"></span>
<select name="delegatedTo"> <select name="delegatedTo">
@@ -297,13 +311,20 @@ p-6 w-full">
<input class="hidden" name="id" value="00000000-0000-0000-0000-000000000003"> <input class="hidden" name="id" value="00000000-0000-0000-0000-000000000003">
Name<label class="input w-full"><span class="label"></span> Name<label class="input w-full"><span class="label"></span>
<input name="name" type="text" placeholder="Name" required autofocus value="Family Room"> <input name="name" type="text" placeholder="Name" required autofocus value="Family Room">
Heating Load</label><label class="input w-full"><span class="label"></span> Level</label><label class="input w-full"><span class="label"></span>
<input name="level" type="number" placeholder="1 (Optional)" value="" min="-1" step="1"></label>
<div class="text-sm italic -mt-2"><span class="text-primary">Use -1 or 0 for a basement</span></div>
Heating Load<label class="input w-full"><span class="label"></span>
<input name="heatingLoad" type="number" placeholder="1234" required min="0" value="9785.0"> <input name="heatingLoad" type="number" placeholder="1234" required min="0" value="9785.0">
Cooling Total</label><label class="input w-full"><span class="label"></span> Cooling Total</label><label class="input w-full"><span class="label"></span>
<input name="coolingTotal" type="number" placeholder="1234 (Optional)" min="0" value="7446.0"> <input name="coolingTotal" type="number" placeholder="1234 (Optional)" min="0" value="7446.0">
Cooling Sensible</label><label class="input w-full"><span class="label"></span> Cooling Sensible</label><label class="input w-full"><span class="label"></span>
<input name="coolingSensible" type="number" placeholder="1234 (Optional)" min="0" value=""> <input name="coolingSensible" type="number" placeholder="1234 (Optional)" min="0" value=""></label>
Registers</label><label class="input w-full"><span class="label"></span> <div class="text-primary text-sm italic -mt-2">
<p>Should enter at least one of the cooling loads.</p>
<p>Both are also acceptable.</p>
</div>
Registers<label class="input w-full"><span class="label"></span>
<input name="registerCount" type="number" min="1" required value="3" id="registerCount"> <input name="registerCount" type="number" min="1" required value="3" id="registerCount">
Room</label><label class="select w-full"><span class="label"></span> Room</label><label class="select w-full"><span class="label"></span>
<select name="delegatedTo"> <select name="delegatedTo">
@@ -360,13 +381,20 @@ p-6 w-full">
<input class="hidden" name="id" value="00000000-0000-0000-0000-000000000004"> <input class="hidden" name="id" value="00000000-0000-0000-0000-000000000004">
Name<label class="input w-full"><span class="label"></span> Name<label class="input w-full"><span class="label"></span>
<input name="name" type="text" placeholder="Name" required autofocus value="Kitchen"> <input name="name" type="text" placeholder="Name" required autofocus value="Kitchen">
Heating Load</label><label class="input w-full"><span class="label"></span> Level</label><label class="input w-full"><span class="label"></span>
<input name="level" type="number" placeholder="1 (Optional)" value="" min="-1" step="1"></label>
<div class="text-sm italic -mt-2"><span class="text-primary">Use -1 or 0 for a basement</span></div>
Heating Load<label class="input w-full"><span class="label"></span>
<input name="heatingLoad" type="number" placeholder="1234" required min="0" value="4518.0"> <input name="heatingLoad" type="number" placeholder="1234" required min="0" value="4518.0">
Cooling Total</label><label class="input w-full"><span class="label"></span> Cooling Total</label><label class="input w-full"><span class="label"></span>
<input name="coolingTotal" type="number" placeholder="1234 (Optional)" min="0" value="5096.0"> <input name="coolingTotal" type="number" placeholder="1234 (Optional)" min="0" value="5096.0">
Cooling Sensible</label><label class="input w-full"><span class="label"></span> Cooling Sensible</label><label class="input w-full"><span class="label"></span>
<input name="coolingSensible" type="number" placeholder="1234 (Optional)" min="0" value=""> <input name="coolingSensible" type="number" placeholder="1234 (Optional)" min="0" value=""></label>
Registers</label><label class="input w-full"><span class="label"></span> <div class="text-primary text-sm italic -mt-2">
<p>Should enter at least one of the cooling loads.</p>
<p>Both are also acceptable.</p>
</div>
Registers<label class="input w-full"><span class="label"></span>
<input name="registerCount" type="number" min="1" required value="2" id="registerCount"> <input name="registerCount" type="number" min="1" required value="2" id="registerCount">
Room</label><label class="select w-full"><span class="label"></span> Room</label><label class="select w-full"><span class="label"></span>
<select name="delegatedTo"> <select name="delegatedTo">
@@ -423,13 +451,20 @@ p-6 w-full">
<input class="hidden" name="id" value="00000000-0000-0000-0000-000000000005"> <input class="hidden" name="id" value="00000000-0000-0000-0000-000000000005">
Name<label class="input w-full"><span class="label"></span> Name<label class="input w-full"><span class="label"></span>
<input name="name" type="text" placeholder="Name" required autofocus value="Living Room"> <input name="name" type="text" placeholder="Name" required autofocus value="Living Room">
Heating Load</label><label class="input w-full"><span class="label"></span> Level</label><label class="input w-full"><span class="label"></span>
<input name="level" type="number" placeholder="1 (Optional)" value="" min="-1" step="1"></label>
<div class="text-sm italic -mt-2"><span class="text-primary">Use -1 or 0 for a basement</span></div>
Heating Load<label class="input w-full"><span class="label"></span>
<input name="heatingLoad" type="number" placeholder="1234" required min="0" value="7553.0"> <input name="heatingLoad" type="number" placeholder="1234" required min="0" value="7553.0">
Cooling Total</label><label class="input w-full"><span class="label"></span> Cooling Total</label><label class="input w-full"><span class="label"></span>
<input name="coolingTotal" type="number" placeholder="1234 (Optional)" min="0" value="6829.0"> <input name="coolingTotal" type="number" placeholder="1234 (Optional)" min="0" value="6829.0">
Cooling Sensible</label><label class="input w-full"><span class="label"></span> Cooling Sensible</label><label class="input w-full"><span class="label"></span>
<input name="coolingSensible" type="number" placeholder="1234 (Optional)" min="0" value=""> <input name="coolingSensible" type="number" placeholder="1234 (Optional)" min="0" value=""></label>
Registers</label><label class="input w-full"><span class="label"></span> <div class="text-primary text-sm italic -mt-2">
<p>Should enter at least one of the cooling loads.</p>
<p>Both are also acceptable.</p>
</div>
Registers<label class="input w-full"><span class="label"></span>
<input name="registerCount" type="number" min="1" required value="2" id="registerCount"> <input name="registerCount" type="number" min="1" required value="2" id="registerCount">
Room</label><label class="select w-full"><span class="label"></span> Room</label><label class="select w-full"><span class="label"></span>
<select name="delegatedTo"> <select name="delegatedTo">
@@ -486,13 +521,20 @@ p-6 w-full">
<input class="hidden" name="id" value="00000000-0000-0000-0000-000000000006"> <input class="hidden" name="id" value="00000000-0000-0000-0000-000000000006">
Name<label class="input w-full"><span class="label"></span> Name<label class="input w-full"><span class="label"></span>
<input name="name" type="text" placeholder="Name" required autofocus value="Master"> <input name="name" type="text" placeholder="Name" required autofocus value="Master">
Heating Load</label><label class="input w-full"><span class="label"></span> Level</label><label class="input w-full"><span class="label"></span>
<input name="level" type="number" placeholder="1 (Optional)" value="" min="-1" step="1"></label>
<div class="text-sm italic -mt-2"><span class="text-primary">Use -1 or 0 for a basement</span></div>
Heating Load<label class="input w-full"><span class="label"></span>
<input name="heatingLoad" type="number" placeholder="1234" required min="0" value="8202.0"> <input name="heatingLoad" type="number" placeholder="1234" required min="0" value="8202.0">
Cooling Total</label><label class="input w-full"><span class="label"></span> Cooling Total</label><label class="input w-full"><span class="label"></span>
<input name="coolingTotal" type="number" placeholder="1234 (Optional)" min="0" value="2076.0"> <input name="coolingTotal" type="number" placeholder="1234 (Optional)" min="0" value="2076.0">
Cooling Sensible</label><label class="input w-full"><span class="label"></span> Cooling Sensible</label><label class="input w-full"><span class="label"></span>
<input name="coolingSensible" type="number" placeholder="1234 (Optional)" min="0" value=""> <input name="coolingSensible" type="number" placeholder="1234 (Optional)" min="0" value=""></label>
Registers</label><label class="input w-full"><span class="label"></span> <div class="text-primary text-sm italic -mt-2">
<p>Should enter at least one of the cooling loads.</p>
<p>Both are also acceptable.</p>
</div>
Registers<label class="input w-full"><span class="label"></span>
<input name="registerCount" type="number" min="1" required value="2" id="registerCount"> <input name="registerCount" type="number" min="1" required value="2" id="registerCount">
Room</label><label class="select w-full"><span class="label"></span> Room</label><label class="select w-full"><span class="label"></span>
<select name="delegatedTo"> <select name="delegatedTo">
@@ -519,13 +561,20 @@ p-6 w-full">
<form class="grid grid-cols-1 gap-4" hx-post="/projects/00000000-0000-0000-0000-000000000000/rooms" hx-target="body" hx-swap="outerHTML"> <form class="grid grid-cols-1 gap-4" hx-post="/projects/00000000-0000-0000-0000-000000000000/rooms" hx-target="body" hx-swap="outerHTML">
<label class="input w-full"><span class="label">Name</span> <label class="input w-full"><span class="label">Name</span>
<input name="name" type="text" placeholder="Name" required autofocus value=""> <input name="name" type="text" placeholder="Name" required autofocus value="">
Heating Load</label><label class="input w-full"><span class="label"></span> Level</label><label class="input w-full"><span class="label"></span>
<input name="level" type="number" placeholder="1 (Optional)" value="" min="-1" step="1"></label>
<div class="text-sm italic -mt-2"><span class="text-primary">Use -1 or 0 for a basement</span></div>
Heating Load<label class="input w-full"><span class="label"></span>
<input name="heatingLoad" type="number" placeholder="1234" required min="0" value=""> <input name="heatingLoad" type="number" placeholder="1234" required min="0" value="">
Cooling Total</label><label class="input w-full"><span class="label"></span> Cooling Total</label><label class="input w-full"><span class="label"></span>
<input name="coolingTotal" type="number" placeholder="1234 (Optional)" min="0" value=""> <input name="coolingTotal" type="number" placeholder="1234 (Optional)" min="0" value="">
Cooling Sensible</label><label class="input w-full"><span class="label"></span> Cooling Sensible</label><label class="input w-full"><span class="label"></span>
<input name="coolingSensible" type="number" placeholder="1234 (Optional)" min="0" value=""> <input name="coolingSensible" type="number" placeholder="1234 (Optional)" min="0" value=""></label>
Registers</label><label class="input w-full"><span class="label"></span> <div class="text-primary text-sm italic -mt-2">
<p>Should enter at least one of the cooling loads.</p>
<p>Both are also acceptable.</p>
</div>
Registers<label class="input w-full"><span class="label"></span>
<input name="registerCount" type="number" min="1" required value="1" id="registerCount"> <input name="registerCount" type="number" min="1" required value="1" id="registerCount">
Room</label><label class="select w-full"><span class="label"></span> Room</label><label class="select w-full"><span class="label"></span>
<select name="delegatedTo"> <select name="delegatedTo">
@@ -676,11 +725,25 @@ is-drawer-close:justify-center is-drawer-close:mx-auto is-drawer-close:space-y-2
</div> </div>
</main> </main>
<div class="bottom-0 left-0 bg-error"> <div class="bottom-0 left-0 bg-error">
<footer class="footer sm:footer-horizontal footer-center <footer class="footer footer-horizontal footer-center
bg-base-300 text-base-content p-4"> bg-base-300 text-base-content p-4">
<aside> <aside class="grid-flow-row items-center">
<p>Copyright © 2026 - All rights reserved by Michael Housh</p> <div class="flex mx-auto">
Openly licensed via CC-BY-NC-SA 4.0<a class="btn btn-ghost" href="https://git.housh.dev/michael/swift-duct-calc/src/branch/main/LICENSE" target="_blank"></a> <a class="btn btn-ghost" href="mailto:support@ductcalc.pro"> <svg class="h-[1em] opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g
stroke-linejoin="round"
stroke-linecap="round"
stroke-width="2.5"
fill="none"
stroke="currentColor"
>
<rect width="20" height="16" x="2" y="4" rx="2"></rect>
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"></path>
</g>
</svg><span>support@ductcalc.pro</span></a>
</div>
Openly licensed via CC-BY-NC-SA 4.0<a class="btn btn-ghost mx-auto" href="https://github.com/m-housh/swift-duct-calc/src/branch/main/LICENSE" target="_blank"></a>
<p class="">Copyright © 2026 - All rights reserved by Michael Housh</p>
</aside> </aside>
</footer> </footer>
</div> </div>

View File

@@ -36,7 +36,7 @@
<div class="drawer-content overflow-auto"> <div class="drawer-content overflow-auto">
<nav class="navbar w-full bg-base-300 text-base-content shadow-sm mb-4"> <nav class="navbar w-full bg-base-300 text-base-content shadow-sm mb-4">
<div class="flex flex-1 space-x-4 items-center"> <div class="flex flex-1 space-x-4 items-center">
<div class="tooltip tooltip-right" data-tip="Open sidebar"><label for="my-drawer-1" class="size-7 btn btn-square btn-ghost hover:bg-neutral hover:text-white" aria-label="open sidebar"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-linejoin="round" stroke-linecap="round" stroke-width="2" fill="none" stroke="currentColor" class="my-1.5 inline-block"><path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path><path d="M9 4v16"></path><path d="M14 10l2 2l-2 2"></path></svg></label></div> <div class="tooltip tooltip-right" data-tip="Open / close sidebar"><label for="my-drawer-1" class="size-7 btn btn-square btn-ghost hover:bg-neutral hover:text-white" aria-label="open / close sidebar"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-linejoin="round" stroke-linecap="round" stroke-width="2" fill="none" stroke="currentColor" class="my-1.5 inline-block"><path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path><path d="M9 4v16"></path><path d="M14 10l2 2l-2 2"></path></svg></label></div>
<div class="tooltip tooltip-right" data-tip="Projects"> <div class="tooltip tooltip-right" data-tip="Projects">
<a class="flex w-fit h-fit text-2xl items-end px-4 py-2 btn btn-square btn-ghost hover:bg-neutral hover:text-white" href="/projects"> <a class="flex w-fit h-fit text-2xl items-end px-4 py-2 btn btn-square btn-ghost hover:bg-neutral hover:text-white" href="/projects">
<img src="/images/mand_logo_sm.webp"> <img src="/images/mand_logo_sm.webp">
@@ -231,11 +231,25 @@ is-drawer-close:justify-center is-drawer-close:mx-auto is-drawer-close:space-y-2
</div> </div>
</main> </main>
<div class="bottom-0 left-0 bg-error"> <div class="bottom-0 left-0 bg-error">
<footer class="footer sm:footer-horizontal footer-center <footer class="footer footer-horizontal footer-center
bg-base-300 text-base-content p-4"> bg-base-300 text-base-content p-4">
<aside> <aside class="grid-flow-row items-center">
<p>Copyright © 2026 - All rights reserved by Michael Housh</p> <div class="flex mx-auto">
Openly licensed via CC-BY-NC-SA 4.0<a class="btn btn-ghost" href="https://git.housh.dev/michael/swift-duct-calc/src/branch/main/LICENSE" target="_blank"></a> <a class="btn btn-ghost" href="mailto:support@ductcalc.pro"> <svg class="h-[1em] opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g
stroke-linejoin="round"
stroke-linecap="round"
stroke-width="2.5"
fill="none"
stroke="currentColor"
>
<rect width="20" height="16" x="2" y="4" rx="2"></rect>
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"></path>
</g>
</svg><span>support@ductcalc.pro</span></a>
</div>
Openly licensed via CC-BY-NC-SA 4.0<a class="btn btn-ghost mx-auto" href="https://github.com/m-housh/swift-duct-calc/src/branch/main/LICENSE" target="_blank"></a>
<p class="">Copyright © 2026 - All rights reserved by Michael Housh</p>
</aside> </aside>
</footer> </footer>
</div> </div>

View File

@@ -36,7 +36,7 @@
<div class="drawer-content overflow-auto"> <div class="drawer-content overflow-auto">
<nav class="navbar w-full bg-base-300 text-base-content shadow-sm mb-4"> <nav class="navbar w-full bg-base-300 text-base-content shadow-sm mb-4">
<div class="flex flex-1 space-x-4 items-center"> <div class="flex flex-1 space-x-4 items-center">
<div class="tooltip tooltip-right" data-tip="Open sidebar"><label for="my-drawer-1" class="size-7 btn btn-square btn-ghost hover:bg-neutral hover:text-white" aria-label="open sidebar"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-linejoin="round" stroke-linecap="round" stroke-width="2" fill="none" stroke="currentColor" class="my-1.5 inline-block"><path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path><path d="M9 4v16"></path><path d="M14 10l2 2l-2 2"></path></svg></label></div> <div class="tooltip tooltip-right" data-tip="Open / close sidebar"><label for="my-drawer-1" class="size-7 btn btn-square btn-ghost hover:bg-neutral hover:text-white" aria-label="open / close sidebar"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-linejoin="round" stroke-linecap="round" stroke-width="2" fill="none" stroke="currentColor" class="my-1.5 inline-block"><path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path><path d="M9 4v16"></path><path d="M14 10l2 2l-2 2"></path></svg></label></div>
<div class="tooltip tooltip-right" data-tip="Projects"> <div class="tooltip tooltip-right" data-tip="Projects">
<a class="flex w-fit h-fit text-2xl items-end px-4 py-2 btn btn-square btn-ghost hover:bg-neutral hover:text-white" href="/projects"> <a class="flex w-fit h-fit text-2xl items-end px-4 py-2 btn btn-square btn-ghost hover:bg-neutral hover:text-white" href="/projects">
<img src="/images/mand_logo_sm.webp"> <img src="/images/mand_logo_sm.webp">
@@ -361,11 +361,25 @@ is-drawer-close:justify-center is-drawer-close:mx-auto is-drawer-close:space-y-2
</div> </div>
</main> </main>
<div class="bottom-0 left-0 bg-error"> <div class="bottom-0 left-0 bg-error">
<footer class="footer sm:footer-horizontal footer-center <footer class="footer footer-horizontal footer-center
bg-base-300 text-base-content p-4"> bg-base-300 text-base-content p-4">
<aside> <aside class="grid-flow-row items-center">
<p>Copyright © 2026 - All rights reserved by Michael Housh</p> <div class="flex mx-auto">
Openly licensed via CC-BY-NC-SA 4.0<a class="btn btn-ghost" href="https://git.housh.dev/michael/swift-duct-calc/src/branch/main/LICENSE" target="_blank"></a> <a class="btn btn-ghost" href="mailto:support@ductcalc.pro"> <svg class="h-[1em] opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g
stroke-linejoin="round"
stroke-linecap="round"
stroke-width="2.5"
fill="none"
stroke="currentColor"
>
<rect width="20" height="16" x="2" y="4" rx="2"></rect>
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"></path>
</g>
</svg><span>support@ductcalc.pro</span></a>
</div>
Openly licensed via CC-BY-NC-SA 4.0<a class="btn btn-ghost mx-auto" href="https://github.com/m-housh/swift-duct-calc/src/branch/main/LICENSE" target="_blank"></a>
<p class="">Copyright © 2026 - All rights reserved by Michael Housh</p>
</aside> </aside>
</footer> </footer>
</div> </div>

View File

@@ -36,7 +36,7 @@
<div class="drawer-content overflow-auto"> <div class="drawer-content overflow-auto">
<nav class="navbar w-full bg-base-300 text-base-content shadow-sm mb-4"> <nav class="navbar w-full bg-base-300 text-base-content shadow-sm mb-4">
<div class="flex flex-1 space-x-4 items-center"> <div class="flex flex-1 space-x-4 items-center">
<div class="tooltip tooltip-right" data-tip="Open sidebar"><label for="my-drawer-1" class="size-7 btn btn-square btn-ghost hover:bg-neutral hover:text-white" aria-label="open sidebar"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-linejoin="round" stroke-linecap="round" stroke-width="2" fill="none" stroke="currentColor" class="my-1.5 inline-block"><path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path><path d="M9 4v16"></path><path d="M14 10l2 2l-2 2"></path></svg></label></div> <div class="tooltip tooltip-right" data-tip="Open / close sidebar"><label for="my-drawer-1" class="size-7 btn btn-square btn-ghost hover:bg-neutral hover:text-white" aria-label="open / close sidebar"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-linejoin="round" stroke-linecap="round" stroke-width="2" fill="none" stroke="currentColor" class="my-1.5 inline-block"><path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path><path d="M9 4v16"></path><path d="M14 10l2 2l-2 2"></path></svg></label></div>
<div class="tooltip tooltip-right" data-tip="Projects"> <div class="tooltip tooltip-right" data-tip="Projects">
<a class="flex w-fit h-fit text-2xl items-end px-4 py-2 btn btn-square btn-ghost hover:bg-neutral hover:text-white" href="/projects"> <a class="flex w-fit h-fit text-2xl items-end px-4 py-2 btn btn-square btn-ghost hover:bg-neutral hover:text-white" href="/projects">
<img src="/images/mand_logo_sm.webp"> <img src="/images/mand_logo_sm.webp">
@@ -437,11 +437,25 @@ is-drawer-close:justify-center is-drawer-close:mx-auto is-drawer-close:space-y-2
</div> </div>
</main> </main>
<div class="bottom-0 left-0 bg-error"> <div class="bottom-0 left-0 bg-error">
<footer class="footer sm:footer-horizontal footer-center <footer class="footer footer-horizontal footer-center
bg-base-300 text-base-content p-4"> bg-base-300 text-base-content p-4">
<aside> <aside class="grid-flow-row items-center">
<p>Copyright © 2026 - All rights reserved by Michael Housh</p> <div class="flex mx-auto">
Openly licensed via CC-BY-NC-SA 4.0<a class="btn btn-ghost" href="https://git.housh.dev/michael/swift-duct-calc/src/branch/main/LICENSE" target="_blank"></a> <a class="btn btn-ghost" href="mailto:support@ductcalc.pro"> <svg class="h-[1em] opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g
stroke-linejoin="round"
stroke-linecap="round"
stroke-width="2.5"
fill="none"
stroke="currentColor"
>
<rect width="20" height="16" x="2" y="4" rx="2"></rect>
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"></path>
</g>
</svg><span>support@ductcalc.pro</span></a>
</div>
Openly licensed via CC-BY-NC-SA 4.0<a class="btn btn-ghost mx-auto" href="https://github.com/m-housh/swift-duct-calc/src/branch/main/LICENSE" target="_blank"></a>
<p class="">Copyright © 2026 - All rights reserved by Michael Housh</p>
</aside> </aside>
</footer> </footer>
</div> </div>

View File

@@ -36,7 +36,7 @@
<div class="drawer-content overflow-auto"> <div class="drawer-content overflow-auto">
<nav class="navbar w-full bg-base-300 text-base-content shadow-sm mb-4"> <nav class="navbar w-full bg-base-300 text-base-content shadow-sm mb-4">
<div class="flex flex-1 space-x-4 items-center"> <div class="flex flex-1 space-x-4 items-center">
<div class="tooltip tooltip-right" data-tip="Open sidebar"><label for="my-drawer-1" class="size-7 btn btn-square btn-ghost hover:bg-neutral hover:text-white" aria-label="open sidebar"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-linejoin="round" stroke-linecap="round" stroke-width="2" fill="none" stroke="currentColor" class="my-1.5 inline-block"><path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path><path d="M9 4v16"></path><path d="M14 10l2 2l-2 2"></path></svg></label></div> <div class="tooltip tooltip-right" data-tip="Open / close sidebar"><label for="my-drawer-1" class="size-7 btn btn-square btn-ghost hover:bg-neutral hover:text-white" aria-label="open / close sidebar"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-linejoin="round" stroke-linecap="round" stroke-width="2" fill="none" stroke="currentColor" class="my-1.5 inline-block"><path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path><path d="M9 4v16"></path><path d="M14 10l2 2l-2 2"></path></svg></label></div>
<div class="tooltip tooltip-right" data-tip="Projects"> <div class="tooltip tooltip-right" data-tip="Projects">
<a class="flex w-fit h-fit text-2xl items-end px-4 py-2 btn btn-square btn-ghost hover:bg-neutral hover:text-white" href="/projects"> <a class="flex w-fit h-fit text-2xl items-end px-4 py-2 btn btn-square btn-ghost hover:bg-neutral hover:text-white" href="/projects">
<img src="/images/mand_logo_sm.webp"> <img src="/images/mand_logo_sm.webp">
@@ -1235,11 +1235,25 @@ is-drawer-close:justify-center is-drawer-close:mx-auto is-drawer-close:space-y-2
</div> </div>
</main> </main>
<div class="bottom-0 left-0 bg-error"> <div class="bottom-0 left-0 bg-error">
<footer class="footer sm:footer-horizontal footer-center <footer class="footer footer-horizontal footer-center
bg-base-300 text-base-content p-4"> bg-base-300 text-base-content p-4">
<aside> <aside class="grid-flow-row items-center">
<p>Copyright © 2026 - All rights reserved by Michael Housh</p> <div class="flex mx-auto">
Openly licensed via CC-BY-NC-SA 4.0<a class="btn btn-ghost" href="https://git.housh.dev/michael/swift-duct-calc/src/branch/main/LICENSE" target="_blank"></a> <a class="btn btn-ghost" href="mailto:support@ductcalc.pro"> <svg class="h-[1em] opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g
stroke-linejoin="round"
stroke-linecap="round"
stroke-width="2.5"
fill="none"
stroke="currentColor"
>
<rect width="20" height="16" x="2" y="4" rx="2"></rect>
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"></path>
</g>
</svg><span>support@ductcalc.pro</span></a>
</div>
Openly licensed via CC-BY-NC-SA 4.0<a class="btn btn-ghost mx-auto" href="https://github.com/m-housh/swift-duct-calc/src/branch/main/LICENSE" target="_blank"></a>
<p class="">Copyright © 2026 - All rights reserved by Michael Housh</p>
</aside> </aside>
</footer> </footer>
</div> </div>

View File

@@ -111,11 +111,25 @@ p-6 w-full pb-6">
</div> </div>
</main> </main>
<div class="bottom-0 left-0 bg-error"> <div class="bottom-0 left-0 bg-error">
<footer class="footer sm:footer-horizontal footer-center <footer class="footer footer-horizontal footer-center
bg-base-300 text-base-content p-4"> bg-base-300 text-base-content p-4">
<aside> <aside class="grid-flow-row items-center">
<p>Copyright © 2026 - All rights reserved by Michael Housh</p> <div class="flex mx-auto">
Openly licensed via CC-BY-NC-SA 4.0<a class="btn btn-ghost" href="https://git.housh.dev/michael/swift-duct-calc/src/branch/main/LICENSE" target="_blank"></a> <a class="btn btn-ghost" href="mailto:support@ductcalc.pro"> <svg class="h-[1em] opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g
stroke-linejoin="round"
stroke-linecap="round"
stroke-width="2.5"
fill="none"
stroke="currentColor"
>
<rect width="20" height="16" x="2" y="4" rx="2"></rect>
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"></path>
</g>
</svg><span>support@ductcalc.pro</span></a>
</div>
Openly licensed via CC-BY-NC-SA 4.0<a class="btn btn-ghost mx-auto" href="https://github.com/m-housh/swift-duct-calc/src/branch/main/LICENSE" target="_blank"></a>
<p class="">Copyright © 2026 - All rights reserved by Michael Housh</p>
</aside> </aside>
</footer> </footer>
</div> </div>

View File

@@ -155,11 +155,25 @@
</div> </div>
</main> </main>
<div class="bottom-0 left-0 bg-error"> <div class="bottom-0 left-0 bg-error">
<footer class="footer sm:footer-horizontal footer-center <footer class="footer footer-horizontal footer-center
bg-base-300 text-base-content p-4"> bg-base-300 text-base-content p-4">
<aside> <aside class="grid-flow-row items-center">
<p>Copyright © 2026 - All rights reserved by Michael Housh</p> <div class="flex mx-auto">
Openly licensed via CC-BY-NC-SA 4.0<a class="btn btn-ghost" href="https://git.housh.dev/michael/swift-duct-calc/src/branch/main/LICENSE" target="_blank"></a> <a class="btn btn-ghost" href="mailto:support@ductcalc.pro"> <svg class="h-[1em] opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g
stroke-linejoin="round"
stroke-linecap="round"
stroke-width="2.5"
fill="none"
stroke="currentColor"
>
<rect width="20" height="16" x="2" y="4" rx="2"></rect>
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"></path>
</g>
</svg><span>support@ductcalc.pro</span></a>
</div>
Openly licensed via CC-BY-NC-SA 4.0<a class="btn btn-ghost mx-auto" href="https://github.com/m-housh/swift-duct-calc/src/branch/main/LICENSE" target="_blank"></a>
<p class="">Copyright © 2026 - All rights reserved by Michael Housh</p>
</aside> </aside>
</footer> </footer>
</div> </div>