feat: Adds page header styles, starts an Alert component.

This commit is contained in:
2026-01-14 23:09:28 -05:00
parent 86307dfa05
commit 1b88f81b5f
15 changed files with 272 additions and 140 deletions

View File

@@ -29,6 +29,7 @@
--text-3xl: 1.875rem; --text-3xl: 1.875rem;
--text-3xl--line-height: calc(2.25 / 1.875); --text-3xl--line-height: calc(2.25 / 1.875);
--font-weight-bold: 700; --font-weight-bold: 700;
--radius-sm: 0.25rem;
--radius-md: 0.375rem; --radius-md: 0.375rem;
--radius-lg: 0.5rem; --radius-lg: 0.5rem;
--ease-out: cubic-bezier(0, 0, 0.2, 1); --ease-out: cubic-bezier(0, 0, 0.2, 1);
@@ -5379,6 +5380,9 @@
.my-6 { .my-6 {
margin-block: calc(var(--spacing) * 6); margin-block: calc(var(--spacing) * 6);
} }
.my-auto {
margin-block: auto;
}
.label { .label {
@layer daisyui.l1.l2.l3 { @layer daisyui.l1.l2.l3 {
display: inline-flex; display: inline-flex;
@@ -6893,6 +6897,12 @@
margin-inline-end: calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-x-reverse))); margin-inline-end: calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-x-reverse)));
} }
} }
.gap-y-4 {
row-gap: calc(var(--spacing) * 4);
}
.gap-y-6 {
row-gap: calc(var(--spacing) * 6);
}
.overflow-auto { .overflow-auto {
overflow: auto; overflow: auto;
} }
@@ -7005,6 +7015,9 @@
.rounded-selector { .rounded-selector {
border-radius: var(--radius-selector); border-radius: var(--radius-selector);
} }
.rounded-sm {
border-radius: var(--radius-sm);
}
.rounded-t-box { .rounded-t-box {
border-top-left-radius: var(--radius-box); border-top-left-radius: var(--radius-box);
border-top-right-radius: var(--radius-box); border-top-right-radius: var(--radius-box);
@@ -7177,6 +7190,14 @@
border-style: var(--tw-border-style); border-style: var(--tw-border-style);
border-width: 1px; border-width: 1px;
} }
.border-2 {
border-style: var(--tw-border-style);
border-width: 2px;
}
.border-y {
border-block-style: var(--tw-border-style);
border-block-width: 1px;
}
.border-t { .border-t {
border-top-style: var(--tw-border-style); border-top-style: var(--tw-border-style);
border-top-width: 1px; border-top-width: 1px;
@@ -7307,6 +7328,12 @@
.border-gray-200 { .border-gray-200 {
border-color: var(--color-gray-200); border-color: var(--color-gray-200);
} }
.border-primary {
border-color: var(--color-primary);
}
.border-secondary {
border-color: var(--color-secondary);
}
.menu-active { .menu-active {
:where(:not(ul, details, .menu-title, .btn))& { :where(:not(ul, details, .menu-title, .btn))& {
@layer daisyui.l1.l2 { @layer daisyui.l1.l2 {
@@ -7467,9 +7494,15 @@
.bg-base-300 { .bg-base-300 {
background-color: var(--color-base-300); background-color: var(--color-base-300);
} }
.bg-primary {
background-color: var(--color-primary);
}
.bg-red-500 { .bg-red-500 {
background-color: var(--color-red-500); background-color: var(--color-red-500);
} }
.bg-secondary {
background-color: var(--color-secondary);
}
.bg-white { .bg-white {
background-color: var(--color-white); background-color: var(--color-white);
} }
@@ -7761,6 +7794,9 @@
.p-4 { .p-4 {
padding: calc(var(--spacing) * 4); padding: calc(var(--spacing) * 4);
} }
.p-6 {
padding: calc(var(--spacing) * 6);
}
.menu-title { .menu-title {
@layer daisyui.l1.l2.l3 { @layer daisyui.l1.l2.l3 {
padding-inline: calc(0.25rem * 3); padding-inline: calc(0.25rem * 3);
@@ -8554,6 +8590,12 @@
.text-primary { .text-primary {
color: var(--color-primary); color: var(--color-primary);
} }
.text-primary-content {
color: var(--color-primary-content);
}
.text-secondary-content {
color: var(--color-secondary-content);
}
.text-slate-900 { .text-slate-900 {
color: var(--color-slate-900); color: var(--color-slate-900);
} }

View File

@@ -0,0 +1,28 @@
import Elementary
public struct Alert<Content: HTML>: HTML {
let inner: Content
public init(@HTMLBuilder content: () -> Content) {
self.inner = content()
}
public var body: some HTML<HTMLTag.div> {
div(.class("flex space-x-2")) {
SVG(.triangleAlert)
inner
}
}
}
extension Alert: Sendable where Content: Sendable {}
extension Alert where Content == p<HTMLText> {
public init(_ description: String) {
self.init {
p { description }
}
}
}

View File

@@ -65,7 +65,7 @@ public struct EditButton: HTML, Sendable {
} }
public var body: some HTML<HTMLTag.button> { public var body: some HTML<HTMLTag.button> {
button(.class("btn hover:btn-success"), .type(type)) { button(.class("btn"), .type(type)) {
div(.class("flex")) { div(.class("flex")) {
if let title { if let title {
span(.class("pe-2")) { title } span(.class("pe-2")) { title }
@@ -83,7 +83,7 @@ public struct PlusButton: HTML, Sendable {
public var body: some HTML<HTMLTag.button> { public var body: some HTML<HTMLTag.button> {
button( button(
.type(.button), .type(.button),
.class("btn btn-primary btn-circle text-xl") .class("btn")
) { SVG(.circlePlus) } ) { SVG(.circlePlus) }
} }
} }

View File

@@ -45,4 +45,8 @@ extension HTML where Tag: HTMLTrait.Attributes.Global {
public func badge() -> _AttributedElement<Self> { public func badge() -> _AttributedElement<Self> {
attributes(.class("badge badge-lg badge-outline font-bold")) attributes(.class("badge badge-lg badge-outline font-bold"))
} }
public func hidden(when shouldHide: Bool) -> _AttributedElement<Self> {
attributes(.class("hidden"), when: shouldHide)
}
} }

View File

@@ -1,5 +1,27 @@
import Elementary import Elementary
public struct PageTitleRow<Content: HTML>: HTML, Sendable where Content: Sendable {
let inner: Content
public init(@HTMLBuilder content: () -> Content) {
self.inner = content()
}
public var body: some HTML<HTMLTag.div> {
div(
.class(
"""
flex justify-between bg-secondary border-2 border-primary rounded-sm shadow-sm
p-6 w-full
"""
)
) {
inner
}
}
}
public struct PageTitle: HTML, Sendable { public struct PageTitle: HTML, Sendable {
let title: String let title: String

View File

@@ -33,6 +33,7 @@ extension SVG {
case squareFunction case squareFunction
case squarePen case squarePen
case trash case trash
case triangleAlert
case user case user
case wind case wind
@@ -135,6 +136,10 @@ extension SVG {
return """ return """
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash2-icon lucide-trash-2"><path d="M10 11v6"/><path d="M14 11v6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash2-icon lucide-trash-2"><path d="M10 11v6"/><path d="M14 11v6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
""" """
case .triangleAlert:
return """
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-triangle-alert-icon lucide-triangle-alert"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>
"""
case .user: case .user:
return """ return """
<svg class="h-[1em] opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <svg class="h-[1em] opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">

View File

@@ -1,6 +1,18 @@
import Elementary import Elementary
public struct Tooltip<Inner: HTML & Sendable>: HTML, Sendable { extension HTML {
public func tooltip(
_ tip: String,
position: TooltipPosition = .default
) -> Tooltip<Self> {
Tooltip(tip, position: position) {
self
}
}
}
public struct Tooltip<Inner: HTML>: HTML {
let tooltip: String let tooltip: String
let position: TooltipPosition let position: TooltipPosition
@@ -26,6 +38,8 @@ public struct Tooltip<Inner: HTML & Sendable>: HTML, Sendable {
} }
} }
extension Tooltip: Sendable where Inner: Sendable {}
public enum TooltipPosition: String, CaseIterable, Sendable { public enum TooltipPosition: String, CaseIterable, Sendable {
public static let `default` = Self.left public static let `default` = Self.left

View File

@@ -31,13 +31,12 @@ struct ComponentPressureLossesView: HTML, Sendable {
th { "Value" } th { "Value" }
th { th {
div(.class("flex justify-end mx-auto")) { div(.class("flex justify-end mx-auto")) {
Tooltip("Add Component Loss") { PlusButton()
PlusButton() .attributes(
.attributes( .class("btn-primary text-2xl me-2"),
.class("btn-ghost text-2xl me-2"), .showModal(id: ComponentLossForm.id())
.showModal(id: ComponentLossForm.id()) )
) .tooltip("Add component loss")
}
} }
} }
} }

View File

@@ -3,8 +3,6 @@ import ElementaryHTMX
import ManualDCore import ManualDCore
import Styleguide import Styleguide
// TODO: Add trunk size table.
struct DuctSizingView: HTML, Sendable { struct DuctSizingView: HTML, Sendable {
@Environment(ProjectViewValue.$projectID) var projectID @Environment(ProjectViewValue.$projectID) var projectID
@@ -14,32 +12,40 @@ struct DuctSizingView: HTML, Sendable {
var body: some HTML { var body: some HTML {
div(.class("space-y-4")) { div(.class("space-y-4")) {
PageTitle { "Duct Sizes" } PageTitleRow {
div(.class("space-y-4")) {
PageTitle("Duct Sizes")
if rooms.count == 0 { Alert(
p(.class("text-error italic")) { """
"Must complete all the previous sections to display duct sizing calculations." Must complete all the previous sections to display duct sizing calculations.
} """
} else {
RoomsTable(rooms: rooms)
div(.class("divider mb-6")) {}
}
Row {
h2(.class("text-2xl font-bold")) {
"Trunk / Runout Sizes"
}
PlusButton()
.attributes(
.class("me-6"),
.showModal(id: TrunkSizeForm.id())
) )
.hidden(when: rooms.count > 0)
.attributes(.class("text-error font-bold italic"))
}
} }
if trunks.count > 0 { if rooms.count != 0 {
div(.class("divider -mt-2")) {} RoomsTable(rooms: rooms)
TrunkTable(trunks: trunks, rooms: rooms)
PageTitleRow {
PageTitle {
"Trunk / Runout Sizes"
}
PlusButton()
.attributes(
.class("btn-primary"),
.showModal(id: TrunkSizeForm.id())
)
.tooltip("Add trunk / runout")
}
if trunks.count > 0 {
TrunkTable(trunks: trunks, rooms: rooms)
}
} }
TrunkSizeForm(rooms: rooms, dismiss: true) TrunkSizeForm(rooms: rooms, dismiss: true)

View File

@@ -21,13 +21,14 @@ struct EffectiveLengthsView: HTML, Sendable {
var body: some HTML { var body: some HTML {
div(.class("space-y-4")) { div(.class("space-y-4")) {
Row { PageTitleRow {
PageTitle { "Equivalent Lengths" } PageTitle { "Equivalent Lengths" }
PlusButton() PlusButton()
.attributes( .attributes(
.class("btn-ghost me-4"), .class("btn-primary"),
.showModal(id: EffectiveLengthForm.id(nil)) .showModal(id: EffectiveLengthForm.id(nil))
) )
.tooltip("Add equivalent length")
} }
.attributes(.class("pb-6")) .attributes(.class("pb-6"))

View File

@@ -12,16 +12,15 @@ struct EquipmentInfoView: HTML, Sendable {
.id("equipmentInfo") .id("equipmentInfo")
) { ) {
Row { PageTitleRow {
PageTitle { "Equipment Info" } PageTitle { "Equipment Details" }
Tooltip("Edit equipment info") { EditButton()
EditButton() .attributes(
.attributes( .class("btn-primary"),
.class("btn-ghost"), .showModal(id: EquipmentInfoForm.id)
.showModal(id: EquipmentInfoForm.id) )
) .tooltip("Edit equipment details")
}
} }
if let equipmentInfo { if let equipmentInfo {

View File

@@ -42,60 +42,75 @@ struct FrictionRateView: HTML, Sendable {
var body: some HTML { var body: some HTML {
div(.class("space-y-6")) { div(.class("space-y-6")) {
div(.class("grid grid-cols-2 px-4")) { PageTitleRow {
div(.class("grid grid-cols-2 px-4 gap-y-4")) {
PageTitle { "Friction Rate" } PageTitle { "Friction Rate" }
div(.class("space-y-4 justify-end")) { div(.class("space-y-4 justify-end")) {
if let frictionRateDesignValue { if let frictionRateDesignValue {
LabeledContent("Friction Rate Design Value") { LabeledContent("Friction Rate Design Value") {
Badge(number: frictionRateDesignValue, digits: 2) Badge(number: frictionRateDesignValue, digits: 2)
.attributes(.class("\(badgeColor)")) .attributes(.class("\(badgeColor)"))
}
.attributes(.class("justify-end"))
}
if let availableStaticPressure {
LabeledContent("Available Static Pressure") {
Badge(number: availableStaticPressure, digits: 2)
}
.attributes(.class("justify-end"))
} }
.attributes(.class("justify-end"))
} }
if let availableStaticPressure { div(.class("text-error font-bold italic col-span-2")) {
LabeledContent("Available Static Pressure") { Alert {
Badge(number: availableStaticPressure, digits: 2) p {
"Must complete previous sections."
}
} }
.attributes(.class("justify-end")) .hidden(
when: availableStaticPressure != nil && frictionRateDesignValue != nil
)
Alert {
p {
"No component pressures losses"
}
}
.hidden(when: componentLosses.totalComponentPressureLoss > 0)
Alert {
p(.class("block")) {
"Calculated friction rate is below 0.02. The fan may not deliver the required CFM."
br()
" * Increase the blower speed"
br()
" * Increase the blower size"
br()
" * Decrease the Total Effective Length (TEL)"
}
}
.hidden(when: !showLowErrors)
Alert {
p(.class("block")) {
"Calculated friction rate is above 0.18. The fan may deliver too many CFM."
br()
" * Decrease the blower speed"
br()
" * Decreae the blower size"
br()
" * Increase the Total Effective Length (TEL)"
}
}
.hidden(when: !showHighErrors)
} }
} }
} }
div(.class("text-error italic")) {
p {
"No component pressures losses"
}
.attributes(.class("hidden"), when: componentLosses.totalComponentPressureLoss > 0)
p {
"Calculated friction rate is below 0.02. The fan may not deliver the required CFM."
br()
" * Increase the blower speed"
br()
" * Increase the blower size"
br()
" * Decrease the Total Effective Length (TEL)"
}
.attributes(.class("hidden"), when: !showLowErrors)
p {
"Calculated friction rate is above 0.18. The fan may deliver too many CFM."
br()
" * Decrease the blower speed"
br()
" * Decreae the blower size"
br()
" * Increase the Total Effective Length (TEL)"
}
.attributes(.class("hidden"), when: !showHighErrors)
}
div(.class("divider")) {}
ComponentPressureLossesView( ComponentPressureLossesView(
componentPressureLosses: componentLosses, projectID: projectID componentPressureLosses: componentLosses, projectID: projectID
) )

View File

@@ -82,7 +82,7 @@ public struct MainPage<Inner: HTML>: SendableHTMLDocument where Inner: Sendable
public var body: some HTML { public var body: some HTML {
div(.class("flex flex-col min-h-screen min-w-full")) { div(.class("flex flex-col min-h-screen min-w-full")) {
main(.class("overflow-auto grow")) { main(.class("grow")) {
inner inner
} }

View File

@@ -8,13 +8,15 @@ struct ProjectDetail: HTML, Sendable {
var body: some HTML { var body: some HTML {
div { div {
Row { PageTitleRow {
h1(.class("text-3xl font-bold")) { "Project" } PageTitle { "Project" }
EditButton() EditButton()
.attributes( .attributes(
.class("btn-ghost"), .class("btn-primary"),
.on(.click, "projectForm.showModal()") .on(.click, "projectForm.showModal()")
) )
.tooltip("Edit project", position: .left)
} }
table(.class("table table-zebra text-lg")) { table(.class("table table-zebra text-lg")) {

View File

@@ -13,63 +13,58 @@ struct RoomsView: HTML, Sendable {
var body: some HTML { var body: some HTML {
div(.class("flex w-full flex-col")) { div(.class("flex w-full flex-col")) {
Row { PageTitleRow {
PageTitle { "Room Loads" } div(.class("flex grid grid-cols-3 w-full gap-y-4")) {
div(.class("flex justify-end items-end -my-2")) { div(.class("col-span-2")) {
Tooltip("Project wide sensible heat ratio", position: .left) { PageTitle { "Room Loads" }
button( }
.class(
""" div(.class("flex justify-end grow")) {
justify-end items-end p-4 Tooltip("Project wide sensible heat ratio", position: .left) {
hover:bg-neutral hover:text-white hover:rounded-lg button(
""" .class(
), """
.showModal(id: SHRForm.id) btn btn-primary text-lg font-bold py-2
) { """
LabeledContent { ),
div(.class("flex justify-end items-end space-x-4")) { .showModal(id: SHRForm.id)
Label { ) {
div(.class("flex grow justify-end items-end space-x-4")) {
span {
"Sensible Heat Ratio" "Sensible Heat Ratio"
} }
.attributes(.class("me-8"), when: sensibleHeatRatio == nil) if let sensibleHeatRatio {
} Badge(number: sensibleHeatRatio)
} content: { } else {
if let sensibleHeatRatio { Badge { "set" }
Badge(number: sensibleHeatRatio) }
} else {
SVG(.squarePen)
} }
} }
.attributes(.class("border border-error"), when: sensibleHeatRatio == nil)
} }
.attributes(.class("border rounded-lg border-error"), when: sensibleHeatRatio == nil) }
div(.class("flex items-end space-x-4 font-bold")) {
span(.class("text-lg")) { "Heating Total" }
Badge(number: rooms.heatingTotal, digits: 0)
.attributes(.class("badge-error"))
}
div(.class("flex justify-center items-end space-x-4 my-auto font-bold")) {
span(.class("text-lg")) { "Cooling Total" }
Badge(number: rooms.coolingTotal, digits: 0)
.attributes(.class("badge-success"))
}
div(.class("flex grow justify-end items-end space-x-4 me-4 my-auto font-bold")) {
span(.class("text-lg")) { "Cooling Sensible" }
Badge(number: rooms.coolingSensible(shr: sensibleHeatRatio), digits: 0)
.attributes(.class("badge-info"))
} }
} }
} }
div(.class("flex flex-wrap justify-between mt-6")) {
div(.class("flex items-end space-x-4")) {
Label { "Heating Total" }
Badge(number: rooms.heatingTotal, digits: 0)
.attributes(.class("badge-error"))
}
div(.class("flex items-end space-x-4")) {
Label { "Cooling Total" }
Badge(number: rooms.coolingTotal, digits: 0)
.attributes(.class("badge-success"))
}
div(.class("flex justify-end items-end space-x-4 me-4")) {
Label { "Cooling Sensible" }
Badge(number: rooms.coolingSensible(shr: sensibleHeatRatio), digits: 0)
.attributes(.class("badge-info"))
}
}
// .attributes(.class("mt-6 me-4"))
div(.class("divider")) {}
SHRForm(projectID: projectID, sensibleHeatRatio: sensibleHeatRatio) SHRForm(projectID: projectID, sensibleHeatRatio: sensibleHeatRatio)
table(.class("table table-zebra text-lg"), .id("roomsTable")) { table(.class("table table-zebra text-lg"), .id("roomsTable")) {
@@ -101,7 +96,7 @@ struct RoomsView: HTML, Sendable {
Tooltip("Add Room") { Tooltip("Add Room") {
PlusButton() PlusButton()
.attributes( .attributes(
.class("btn-ghost mx-auto"), .class("btn-primary mx-auto"),
.showModal(id: RoomForm.id()) .showModal(id: RoomForm.id())
) )
.attributes(.class("tooltip-left")) .attributes(.class("tooltip-left"))