From 49af734a970a7b8110377040d940a9611d5fe27c Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Fri, 28 Feb 2025 16:27:39 -0500 Subject: [PATCH] feat: Begins room-pressure calculator --- Dockerfile | 3 +- Public/output.css | 2 +- Sources/Routes/Models/ClimateZone.swift | 31 ++++ .../Routes/Models/FilterPressureDrop.swift | 112 ++++++++++++ Sources/Routes/Models/RoomPressure.swift | 126 +++++++++++++ Sources/Routes/SiteRoutes.swift | 22 +++ Sources/Styleguide/Buttons.swift | 36 ++++ .../Extensions/HTMLAttributeExtensions.swift | 4 + Sources/ViewController/Live.swift | 9 +- .../Views/HVACSystemPerformance.swift | 9 + Sources/ViewController/Views/MainPage.swift | 8 + Sources/ViewController/Views/MoldRisk.swift | 61 ++----- .../ViewController/Views/RoomPressure.swift | 166 ++++++++++++++++++ docker-compose.yml | 4 +- justfile | 5 + 15 files changed, 548 insertions(+), 50 deletions(-) create mode 100644 Sources/Routes/Models/ClimateZone.swift create mode 100644 Sources/Routes/Models/FilterPressureDrop.swift create mode 100644 Sources/Routes/Models/RoomPressure.swift create mode 100644 Sources/ViewController/Views/RoomPressure.swift diff --git a/Dockerfile b/Dockerfile index f0bde23..8ee5b0a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -61,7 +61,8 @@ RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ ca-certificates \ tzdata \ # If your app or its dependencies import FoundationNetworking, also install `libcurl4`. - # libcurl4 \ + libcurl4 \ + curl \ # If your app or its dependencies import FoundationXML, also install `libxml2`. # libxml2 \ && rm -r /var/lib/apt/lists/* diff --git a/Public/output.css b/Public/output.css index 1903685..3ce5866 100644 --- a/Public/output.css +++ b/Public/output.css @@ -1,2 +1,2 @@ /*! tailwindcss v4.0.8 | MIT License | https://tailwindcss.com */ -@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-red-200:oklch(.885 .062 18.334);--color-red-500:oklch(.637 .237 25.331);--color-orange-200:oklch(.901 .076 70.697);--color-orange-500:oklch(.705 .213 47.604);--color-amber-100:oklch(.962 .059 95.617);--color-amber-200:oklch(.924 .12 95.746);--color-amber-500:oklch(.769 .188 70.08);--color-yellow-300:oklch(.905 .182 98.111);--color-yellow-400:oklch(.852 .199 91.936);--color-yellow-800:oklch(.476 .114 61.907);--color-green-100:oklch(.962 .044 156.743);--color-green-200:oklch(.925 .084 155.995);--color-green-500:oklch(.723 .219 149.579);--color-green-600:oklch(.627 .194 149.214);--color-blue-50:oklch(.97 .014 254.604);--color-blue-100:oklch(.932 .032 255.585);--color-blue-200:oklch(.882 .059 254.128);--color-blue-300:oklch(.809 .105 251.813);--color-blue-400:oklch(.707 .165 254.624);--color-blue-500:oklch(.623 .214 259.815);--color-blue-600:oklch(.546 .245 262.881);--color-blue-700:oklch(.488 .243 264.376);--color-blue-800:oklch(.424 .199 265.638);--color-purple-100:oklch(.946 .033 307.174);--color-purple-600:oklch(.558 .288 302.321);--color-slate-100:oklch(.968 .007 247.896);--color-slate-200:oklch(.929 .013 255.508);--color-slate-300:oklch(.869 .022 252.894);--color-slate-400:oklch(.704 .04 256.788);--color-slate-600:oklch(.446 .043 257.281);--color-slate-700:oklch(.372 .044 257.287);--color-gray-100:oklch(.967 .003 264.542);--color-gray-200:oklch(.928 .006 264.531);--color-gray-300:oklch(.872 .01 258.338);--color-gray-400:oklch(.707 .022 261.325);--color-gray-600:oklch(.446 .03 256.802);--color-gray-700:oklch(.373 .034 259.733);--color-gray-800:oklch(.278 .033 256.848);--color-white:#fff;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height:calc(1.5/1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25/1.875);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5/2.25);--font-weight-light:300;--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--font-weight-extrabold:800;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-font-feature-settings:var(--font-sans--font-feature-settings);--default-font-variation-settings:var(--font-sans--font-variation-settings);--default-mono-font-family:var(--font-mono);--default-mono-font-feature-settings:var(--font-mono--font-feature-settings);--default-mono-font-variation-settings:var(--font-mono--font-variation-settings)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}body{line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1;color:color-mix(in oklab,currentColor 50%,transparent)}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}*,:after,:before,::backdrop{border-color:var(--color-gray-200,currentColor)}::file-selector-button{border-color:var(--color-gray-200,currentColor)}}@layer components;@layer utilities{.absolute{position:absolute}.relative{position:relative}.static{position:static}.right-0{right:calc(var(--spacing)*0)}.bottom-0{bottom:calc(var(--spacing)*0)}.z-6{z-index:6}.m-5{margin:calc(var(--spacing)*5)}.mx-5{margin-inline:calc(var(--spacing)*5)}.mx-6{margin-inline:calc(var(--spacing)*6)}.mx-10{margin-inline:calc(var(--spacing)*10)}.mt-1{margin-top:calc(var(--spacing)*1)}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-6{margin-top:calc(var(--spacing)*6)}.mt-8{margin-top:calc(var(--spacing)*8)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.mb-6{margin-bottom:calc(var(--spacing)*6)}.mb-8{margin-bottom:calc(var(--spacing)*8)}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.table{display:table}.h-5{height:calc(var(--spacing)*5)}.h-6{height:calc(var(--spacing)*6)}.h-8{height:calc(var(--spacing)*8)}.h-12{height:calc(var(--spacing)*12)}.min-h-screen{min-height:100vh}.w-5{width:calc(var(--spacing)*5)}.w-6{width:calc(var(--spacing)*6)}.w-8{width:calc(var(--spacing)*8)}.w-12{width:calc(var(--spacing)*12)}.w-full{width:100%}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-2{gap:calc(var(--spacing)*2)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}.gap-6{gap:calc(var(--spacing)*6)}.gap-8{gap:calc(var(--spacing)*8)}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*3)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*4)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*6)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-y-reverse)))}.gap-x-2{column-gap:calc(var(--spacing)*2)}:where(.space-x-8>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*8)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*8)*calc(1 - var(--tw-space-x-reverse)))}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-sm{border-radius:var(--radius-sm)}.rounded-xl{border-radius:var(--radius-xl)}.rounded-e-lg{border-start-end-radius:var(--radius-lg);border-end-end-radius:var(--radius-lg)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-amber-500{border-color:var(--color-amber-500)}.border-blue-500{border-color:var(--color-blue-500)}.border-blue-600{border-color:var(--color-blue-600)}.border-blue-700{border-color:var(--color-blue-700)}.border-gray-300{border-color:var(--color-gray-300)}.border-gray-400{border-color:var(--color-gray-400)}.border-gray-800{border-color:var(--color-gray-800)}.border-green-500{border-color:var(--color-green-500)}.border-green-600{border-color:var(--color-green-600)}.border-orange-500{border-color:var(--color-orange-500)}.border-purple-600{border-color:var(--color-purple-600)}.border-red-500{border-color:var(--color-red-500)}.border-yellow-300{border-color:var(--color-yellow-300)}.border-yellow-800{border-color:var(--color-yellow-800)}.bg-amber-100{background-color:var(--color-amber-100)}.bg-amber-200{background-color:var(--color-amber-200)}.bg-blue-50{background-color:var(--color-blue-50)}.bg-blue-100{background-color:var(--color-blue-100)}.bg-blue-200{background-color:var(--color-blue-200)}.bg-blue-500{background-color:var(--color-blue-500)}.bg-blue-600{background-color:var(--color-blue-600)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-gray-700{background-color:var(--color-gray-700)}.bg-green-100{background-color:var(--color-green-100)}.bg-green-200{background-color:var(--color-green-200)}.bg-orange-200{background-color:var(--color-orange-200)}.bg-purple-100{background-color:var(--color-purple-100)}.bg-red-200{background-color:var(--color-red-200)}.bg-slate-100{background-color:var(--color-slate-100)}.bg-slate-300{background-color:var(--color-slate-300)}.bg-slate-700{background-color:var(--color-slate-700)}.bg-white{background-color:var(--color-white)}.bg-yellow-300{background-color:var(--color-yellow-300)}.p-2{padding:calc(var(--spacing)*2)}.p-4{padding:calc(var(--spacing)*4)}.p-6{padding:calc(var(--spacing)*6)}.p-8{padding:calc(var(--spacing)*8)}.px-4{padding-inline:calc(var(--spacing)*4)}.px-8{padding-inline:calc(var(--spacing)*8)}.py-1{padding-block:calc(var(--spacing)*1)}.py-2{padding-block:calc(var(--spacing)*2)}.py-3{padding-block:calc(var(--spacing)*3)}.py-4{padding-block:calc(var(--spacing)*4)}.ps-2{padding-inline-start:calc(var(--spacing)*2)}.ps-4{padding-inline-start:calc(var(--spacing)*4)}.pe-2{padding-inline-end:calc(var(--spacing)*2)}.pe-3{padding-inline-end:calc(var(--spacing)*3)}.pt-8{padding-top:calc(var(--spacing)*8)}.pb-4{padding-bottom:calc(var(--spacing)*4)}.pb-6{padding-bottom:calc(var(--spacing)*6)}.pb-8{padding-bottom:calc(var(--spacing)*8)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-extrabold{--tw-font-weight:var(--font-weight-extrabold);font-weight:var(--font-weight-extrabold)}.font-light{--tw-font-weight:var(--font-weight-light);font-weight:var(--font-weight-light)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.text-amber-500{color:var(--color-amber-500)}.text-blue-500{color:var(--color-blue-500)}.text-blue-600{color:var(--color-blue-600)}.text-blue-700{color:var(--color-blue-700)}.text-blue-800{color:var(--color-blue-800)}.text-gray-300{color:var(--color-gray-300)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-green-500{color:var(--color-green-500)}.text-green-600{color:var(--color-green-600)}.text-orange-500{color:var(--color-orange-500)}.text-purple-600{color:var(--color-purple-600)}.text-red-500{color:var(--color-red-500)}.text-white{color:var(--color-white)}.text-yellow-300{color:var(--color-yellow-300)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a),0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.filter{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media (hover:hover){.group-hover\:bg-blue-300:is(:where(.group):hover *){background-color:var(--color-blue-300)}.group-hover\:bg-blue-600:is(:where(.group):hover *){background-color:var(--color-blue-600)}.group-hover\:text-blue-600:is(:where(.group):hover *){color:var(--color-blue-600)}.hover\:border-b:hover{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.hover\:bg-blue-200:hover{background-color:var(--color-blue-200)}.hover\:bg-blue-300:hover{background-color:var(--color-blue-300)}.hover\:bg-blue-600:hover{background-color:var(--color-blue-600)}.hover\:bg-yellow-400:hover{background-color:var(--color-yellow-400)}.hover\:text-blue-600:hover{color:var(--color-blue-600)}}.focus\:border-yellow-800:focus{border-color:var(--color-yellow-800)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentColor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-yellow-800:focus{--tw-ring-color:var(--color-yellow-800)}@media (width>=40rem){.sm\:grid{display:grid}.sm\:w-1\/2{width:50%}.sm\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.sm\:gap-4{gap:calc(var(--spacing)*4)}}@media (width>=48rem){.md\:flex{display:flex}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:justify-between{justify-content:space-between}}@media (width>=64rem){.lg\:mx-20{margin-inline:calc(var(--spacing)*20)}.lg\:flex{display:flex}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-8{grid-template-columns:repeat(8,minmax(0,1fr))}.lg\:justify-between{justify-content:space-between}.lg\:gap-x-5{column-gap:calc(var(--spacing)*5)}}@media (prefers-color-scheme:dark){.dark\:border-gray-100{border-color:var(--color-gray-100)}.dark\:border-gray-200{border-color:var(--color-gray-200)}.dark\:border-gray-300{border-color:var(--color-gray-300)}.dark\:border-gray-400{border-color:var(--color-gray-400)}.dark\:border-gray-600{border-color:var(--color-gray-600)}.dark\:border-gray-800{border-color:var(--color-gray-800)}.dark\:bg-amber-200{background-color:var(--color-amber-200)}.dark\:bg-blue-400{background-color:var(--color-blue-400)}.dark\:bg-blue-500{background-color:var(--color-blue-500)}.dark\:bg-blue-600{background-color:var(--color-blue-600)}.dark\:bg-gray-600{background-color:var(--color-gray-600)}.dark\:bg-gray-700{background-color:var(--color-gray-700)}.dark\:bg-gray-800{background-color:var(--color-gray-800)}.dark\:bg-slate-600{background-color:var(--color-slate-600)}.dark\:bg-slate-700{background-color:var(--color-slate-700)}.dark\:bg-yellow-300{background-color:var(--color-yellow-300)}.dark\:text-blue-500{color:var(--color-blue-500)}.dark\:text-gray-200{color:var(--color-gray-200)}.dark\:text-gray-300{color:var(--color-gray-300)}.dark\:text-gray-600{color:var(--color-gray-600)}.dark\:text-slate-200{color:var(--color-slate-200)}.dark\:text-slate-300{color:var(--color-slate-300)}.dark\:text-slate-400{color:var(--color-slate-400)}.dark\:text-slate-600{color:var(--color-slate-600)}.dark\:text-yellow-300{color:var(--color-yellow-300)}}@media (hover:hover){@media (prefers-color-scheme:dark){.group-hover\:dark\:bg-blue-700:is(:where(.group):hover *){background-color:var(--color-blue-700)}.hover\:dark\:bg-blue-600:hover{background-color:var(--color-blue-600)}.hover\:dark\:bg-yellow-400:hover{background-color:var(--color-yellow-400)}}}.\[\&\:hover\]\:border-b:hover{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}}body{font-family:Helvetica}*{box-sizing:border-box;-ms-box-sizing:border-box}nav a:active{border-color:var(--color-yellow-300);color:var(--color-white)}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false} \ No newline at end of file +@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-red-200:oklch(.885 .062 18.334);--color-red-500:oklch(.637 .237 25.331);--color-orange-200:oklch(.901 .076 70.697);--color-orange-500:oklch(.705 .213 47.604);--color-amber-100:oklch(.962 .059 95.617);--color-amber-200:oklch(.924 .12 95.746);--color-amber-500:oklch(.769 .188 70.08);--color-yellow-300:oklch(.905 .182 98.111);--color-yellow-400:oklch(.852 .199 91.936);--color-yellow-800:oklch(.476 .114 61.907);--color-green-100:oklch(.962 .044 156.743);--color-green-200:oklch(.925 .084 155.995);--color-green-500:oklch(.723 .219 149.579);--color-green-600:oklch(.627 .194 149.214);--color-blue-50:oklch(.97 .014 254.604);--color-blue-100:oklch(.932 .032 255.585);--color-blue-200:oklch(.882 .059 254.128);--color-blue-300:oklch(.809 .105 251.813);--color-blue-400:oklch(.707 .165 254.624);--color-blue-500:oklch(.623 .214 259.815);--color-blue-600:oklch(.546 .245 262.881);--color-blue-700:oklch(.488 .243 264.376);--color-blue-800:oklch(.424 .199 265.638);--color-purple-100:oklch(.946 .033 307.174);--color-purple-600:oklch(.558 .288 302.321);--color-slate-100:oklch(.968 .007 247.896);--color-slate-200:oklch(.929 .013 255.508);--color-slate-300:oklch(.869 .022 252.894);--color-slate-400:oklch(.704 .04 256.788);--color-slate-600:oklch(.446 .043 257.281);--color-slate-700:oklch(.372 .044 257.287);--color-gray-100:oklch(.967 .003 264.542);--color-gray-200:oklch(.928 .006 264.531);--color-gray-300:oklch(.872 .01 258.338);--color-gray-400:oklch(.707 .022 261.325);--color-gray-600:oklch(.446 .03 256.802);--color-gray-700:oklch(.373 .034 259.733);--color-gray-800:oklch(.278 .033 256.848);--color-white:#fff;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height:calc(1.5/1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25/1.875);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5/2.25);--font-weight-light:300;--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--font-weight-extrabold:800;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-font-feature-settings:var(--font-sans--font-feature-settings);--default-font-variation-settings:var(--font-sans--font-variation-settings);--default-mono-font-family:var(--font-mono);--default-mono-font-feature-settings:var(--font-mono--font-feature-settings);--default-mono-font-variation-settings:var(--font-mono--font-variation-settings)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}body{line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1;color:color-mix(in oklab,currentColor 50%,transparent)}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}*,:after,:before,::backdrop{border-color:var(--color-gray-200,currentColor)}::file-selector-button{border-color:var(--color-gray-200,currentColor)}}@layer components;@layer utilities{.absolute{position:absolute}.relative{position:relative}.static{position:static}.top-0{top:calc(var(--spacing)*0)}.right-0{right:calc(var(--spacing)*0)}.bottom-0{bottom:calc(var(--spacing)*0)}.z-1{z-index:1}.z-2{z-index:2}.z-5{z-index:5}.z-6{z-index:6}.z-9{z-index:9}.container{width:100%}@media (width>=40rem){.container{max-width:40rem}}@media (width>=48rem){.container{max-width:48rem}}@media (width>=64rem){.container{max-width:64rem}}@media (width>=80rem){.container{max-width:80rem}}@media (width>=96rem){.container{max-width:96rem}}.m-5{margin:calc(var(--spacing)*5)}.m-8{margin:calc(var(--spacing)*8)}.mx-5{margin-inline:calc(var(--spacing)*5)}.mx-6{margin-inline:calc(var(--spacing)*6)}.mx-8{margin-inline:calc(var(--spacing)*8)}.mx-10{margin-inline:calc(var(--spacing)*10)}.my-6{margin-block:calc(var(--spacing)*6)}.me-4{margin-inline-end:calc(var(--spacing)*4)}.mt-1{margin-top:calc(var(--spacing)*1)}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-6{margin-top:calc(var(--spacing)*6)}.mt-8{margin-top:calc(var(--spacing)*8)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.mb-6{margin-bottom:calc(var(--spacing)*6)}.mb-8{margin-bottom:calc(var(--spacing)*8)}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.table{display:table}.h-3{height:calc(var(--spacing)*3)}.h-5{height:calc(var(--spacing)*5)}.h-6{height:calc(var(--spacing)*6)}.h-7{height:calc(var(--spacing)*7)}.h-8{height:calc(var(--spacing)*8)}.h-9{height:calc(var(--spacing)*9)}.h-12{height:calc(var(--spacing)*12)}.min-h-screen{min-height:100vh}.w-5{width:calc(var(--spacing)*5)}.w-6{width:calc(var(--spacing)*6)}.w-8{width:calc(var(--spacing)*8)}.w-12{width:calc(var(--spacing)*12)}.w-full{width:100%}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-2{gap:calc(var(--spacing)*2)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}.gap-6{gap:calc(var(--spacing)*6)}.gap-8{gap:calc(var(--spacing)*8)}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*3)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*4)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*6)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-y-reverse)))}.gap-x-0{column-gap:calc(var(--spacing)*0)}.gap-x-2{column-gap:calc(var(--spacing)*2)}.gap-x-4{column-gap:calc(var(--spacing)*4)}.gap-x-6{column-gap:calc(var(--spacing)*6)}:where(.space-x-8>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*8)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*8)*calc(1 - var(--tw-space-x-reverse)))}.gap-y-2{row-gap:calc(var(--spacing)*2)}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-sm{border-radius:var(--radius-sm)}.rounded-xl{border-radius:var(--radius-xl)}.rounded-s-lg{border-start-start-radius:var(--radius-lg);border-end-start-radius:var(--radius-lg)}.rounded-e-lg{border-start-end-radius:var(--radius-lg);border-end-end-radius:var(--radius-lg)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-amber-500{border-color:var(--color-amber-500)}.border-blue-500{border-color:var(--color-blue-500)}.border-blue-600{border-color:var(--color-blue-600)}.border-blue-700{border-color:var(--color-blue-700)}.border-gray-300{border-color:var(--color-gray-300)}.border-gray-400{border-color:var(--color-gray-400)}.border-gray-800{border-color:var(--color-gray-800)}.border-green-500{border-color:var(--color-green-500)}.border-green-600{border-color:var(--color-green-600)}.border-orange-500{border-color:var(--color-orange-500)}.border-purple-600{border-color:var(--color-purple-600)}.border-red-500{border-color:var(--color-red-500)}.border-yellow-300{border-color:var(--color-yellow-300)}.border-yellow-800{border-color:var(--color-yellow-800)}.bg-amber-100{background-color:var(--color-amber-100)}.bg-amber-200{background-color:var(--color-amber-200)}.bg-blue-50{background-color:var(--color-blue-50)}.bg-blue-100{background-color:var(--color-blue-100)}.bg-blue-200{background-color:var(--color-blue-200)}.bg-blue-500{background-color:var(--color-blue-500)}.bg-blue-600{background-color:var(--color-blue-600)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-gray-700{background-color:var(--color-gray-700)}.bg-green-100{background-color:var(--color-green-100)}.bg-green-200{background-color:var(--color-green-200)}.bg-orange-200{background-color:var(--color-orange-200)}.bg-purple-100{background-color:var(--color-purple-100)}.bg-red-200{background-color:var(--color-red-200)}.bg-slate-100{background-color:var(--color-slate-100)}.bg-slate-300{background-color:var(--color-slate-300)}.bg-slate-700{background-color:var(--color-slate-700)}.bg-white{background-color:var(--color-white)}.bg-yellow-300{background-color:var(--color-yellow-300)}.p-2{padding:calc(var(--spacing)*2)}.p-4{padding:calc(var(--spacing)*4)}.p-6{padding:calc(var(--spacing)*6)}.p-8{padding:calc(var(--spacing)*8)}.p-9{padding:calc(var(--spacing)*9)}.px-4{padding-inline:calc(var(--spacing)*4)}.px-6{padding-inline:calc(var(--spacing)*6)}.px-8{padding-inline:calc(var(--spacing)*8)}.py-1{padding-block:calc(var(--spacing)*1)}.py-2{padding-block:calc(var(--spacing)*2)}.py-3{padding-block:calc(var(--spacing)*3)}.py-4{padding-block:calc(var(--spacing)*4)}.ps-2{padding-inline-start:calc(var(--spacing)*2)}.ps-4{padding-inline-start:calc(var(--spacing)*4)}.pe-2{padding-inline-end:calc(var(--spacing)*2)}.pe-3{padding-inline-end:calc(var(--spacing)*3)}.pt-8{padding-top:calc(var(--spacing)*8)}.pb-4{padding-bottom:calc(var(--spacing)*4)}.pb-6{padding-bottom:calc(var(--spacing)*6)}.pb-8{padding-bottom:calc(var(--spacing)*8)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-extrabold{--tw-font-weight:var(--font-weight-extrabold);font-weight:var(--font-weight-extrabold)}.font-light{--tw-font-weight:var(--font-weight-light);font-weight:var(--font-weight-light)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.text-amber-500{color:var(--color-amber-500)}.text-blue-500{color:var(--color-blue-500)}.text-blue-600{color:var(--color-blue-600)}.text-blue-700{color:var(--color-blue-700)}.text-blue-800{color:var(--color-blue-800)}.text-gray-300{color:var(--color-gray-300)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-green-500{color:var(--color-green-500)}.text-green-600{color:var(--color-green-600)}.text-orange-500{color:var(--color-orange-500)}.text-purple-600{color:var(--color-purple-600)}.text-red-500{color:var(--color-red-500)}.text-white{color:var(--color-white)}.text-yellow-300{color:var(--color-yellow-300)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a),0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.filter{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media (hover:hover){.group-hover\:bg-blue-300:is(:where(.group):hover *){background-color:var(--color-blue-300)}.group-hover\:bg-blue-600:is(:where(.group):hover *){background-color:var(--color-blue-600)}.group-hover\:text-blue-600:is(:where(.group):hover *){color:var(--color-blue-600)}.hover\:border-b:hover{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.hover\:bg-blue-200:hover{background-color:var(--color-blue-200)}.hover\:bg-blue-300:hover{background-color:var(--color-blue-300)}.hover\:bg-blue-600:hover{background-color:var(--color-blue-600)}.hover\:bg-yellow-400:hover{background-color:var(--color-yellow-400)}.hover\:text-blue-600:hover{color:var(--color-blue-600)}}.focus\:border-yellow-800:focus{border-color:var(--color-yellow-800)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentColor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-yellow-800:focus{--tw-ring-color:var(--color-yellow-800)}.active\:border-yellow-300:active{border-color:var(--color-yellow-300)}@media (hover:hover){.enabled\:hover\:bg-blue-600:enabled:hover{background-color:var(--color-blue-600)}.enabled\:hover\:bg-yellow-400:enabled:hover{background-color:var(--color-yellow-400)}}@media (width>=40rem){.sm\:grid{display:grid}.sm\:w-1\/2{width:50%}.sm\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.sm\:gap-4{gap:calc(var(--spacing)*4)}}@media (width>=48rem){.md\:flex{display:flex}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:justify-between{justify-content:space-between}}@media (width>=64rem){.lg\:mx-20{margin-inline:calc(var(--spacing)*20)}.lg\:flex{display:flex}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-8{grid-template-columns:repeat(8,minmax(0,1fr))}.lg\:justify-between{justify-content:space-between}.lg\:gap-x-5{column-gap:calc(var(--spacing)*5)}}@media (prefers-color-scheme:dark){.dark\:border-gray-100{border-color:var(--color-gray-100)}.dark\:border-gray-200{border-color:var(--color-gray-200)}.dark\:border-gray-300{border-color:var(--color-gray-300)}.dark\:border-gray-400{border-color:var(--color-gray-400)}.dark\:border-gray-600{border-color:var(--color-gray-600)}.dark\:border-gray-800{border-color:var(--color-gray-800)}.dark\:bg-amber-200{background-color:var(--color-amber-200)}.dark\:bg-blue-400{background-color:var(--color-blue-400)}.dark\:bg-blue-500{background-color:var(--color-blue-500)}.dark\:bg-blue-600{background-color:var(--color-blue-600)}.dark\:bg-gray-600{background-color:var(--color-gray-600)}.dark\:bg-gray-700{background-color:var(--color-gray-700)}.dark\:bg-gray-800{background-color:var(--color-gray-800)}.dark\:bg-slate-600{background-color:var(--color-slate-600)}.dark\:bg-slate-700{background-color:var(--color-slate-700)}.dark\:bg-yellow-300{background-color:var(--color-yellow-300)}.dark\:text-blue-500{color:var(--color-blue-500)}.dark\:text-gray-200{color:var(--color-gray-200)}.dark\:text-gray-300{color:var(--color-gray-300)}.dark\:text-gray-600{color:var(--color-gray-600)}.dark\:text-slate-200{color:var(--color-slate-200)}.dark\:text-slate-300{color:var(--color-slate-300)}.dark\:text-slate-400{color:var(--color-slate-400)}.dark\:text-slate-600{color:var(--color-slate-600)}.dark\:text-yellow-300{color:var(--color-yellow-300)}}@media (hover:hover){@media (prefers-color-scheme:dark){.group-hover\:dark\:bg-blue-700:is(:where(.group):hover *){background-color:var(--color-blue-700)}.hover\:dark\:bg-blue-600:hover{background-color:var(--color-blue-600)}.hover\:dark\:bg-yellow-400:hover{background-color:var(--color-yellow-400)}}}.\[\&\:hover\]\:border-b:hover{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}}body{font-family:Helvetica}*{box-sizing:border-box;-ms-box-sizing:border-box}nav a:active{border-color:var(--color-yellow-300);color:var(--color-white)}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false} \ No newline at end of file diff --git a/Sources/Routes/Models/ClimateZone.swift b/Sources/Routes/Models/ClimateZone.swift new file mode 100644 index 0000000..2d9c6cb --- /dev/null +++ b/Sources/Routes/Models/ClimateZone.swift @@ -0,0 +1,31 @@ +public enum ClimateZone: String, Codable, Equatable, Sendable { + case dry + case hotHumid + case marine + case moist + + public var zoneIdentifiers: [String] { + switch self { + case .dry: + return ["2B", "3B", "4B", "5B", "6B", "7B"] + case .hotHumid: + return ["1A", "2A"] + case .marine: + return ["3C", "4C"] + case .moist: + return ["3A", "4A", "5A", "6A", "7A"] + } + } + + public var cfmPerTon: Int { + switch self { + case .dry: return 450 + case .hotHumid: return 350 + case .marine, .moist: return 400 + } + } + + public var label: String { + return "\(rawValue.capitalized) (\(zoneIdentifiers.joined(separator: ",")))" + } +} diff --git a/Sources/Routes/Models/FilterPressureDrop.swift b/Sources/Routes/Models/FilterPressureDrop.swift new file mode 100644 index 0000000..c0abfe7 --- /dev/null +++ b/Sources/Routes/Models/FilterPressureDrop.swift @@ -0,0 +1,112 @@ +public enum FilterPressureDrop { + + public enum Request: Codable, Equatable, Sendable { + case basic(Basic) + case fanLaw(FanLaw) + + public struct Basic: Codable, Equatable, Sendable { + + let systemSize: HVACSystemSize + let climateZone: ClimateZone + let filterType: FilterType + let filterWidth: Double + let filterHeight: Double + + public init( + systemSize: HVACSystemSize, + climateZone: ClimateZone, + filterType: FilterPressureDrop.FilterType, + filterWidth: Double, + filterHeight: Double + ) { + self.systemSize = systemSize + self.climateZone = climateZone + self.filterType = filterType + self.filterWidth = filterWidth + self.filterHeight = filterHeight + } + } + + public struct FanLaw: Codable, Equatable, Sendable { + + let filterWidth: Double + let filterHeight: Double + let filterDepth: Double + let ratedAirflow: Double + let ratedPressureDrop: Double + let designAirflow: Double + + public init( + filterWidth: Double, + filterHeight: Double, + filterDepth: Double, + ratedAirflow: Double, + ratedPressureDrop: Double, + designAirflow: Double + ) { + self.filterWidth = filterWidth + self.filterHeight = filterHeight + self.filterDepth = filterDepth + self.ratedAirflow = ratedAirflow + self.ratedPressureDrop = ratedPressureDrop + self.designAirflow = designAirflow + } + } + } + + public enum Result: 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 init(filterArea: Double, feetPerMinute: Double, initialPressureDrop: Double, maxPressureDrop: Double) { + self.filterArea = filterArea + self.feetPerMinute = feetPerMinute + self.initialPressureDrop = initialPressureDrop + self.maxPressureDrop = maxPressureDrop + } + } + + public struct FanLaw: Codable, Equatable, Sendable { + + public let predictedPressureDrop: Double + public let velocityRatio: Double + public let pressureRatio: Double + public let faceVelocity: Double + + public init( + predictedPressureDrop: Double, + velocityRatio: Double, + pressureRatio: Double, + faceVelocity: Double + ) { + self.predictedPressureDrop = predictedPressureDrop + self.velocityRatio = velocityRatio + self.pressureRatio = pressureRatio + self.faceVelocity = faceVelocity + } + } + } + + public enum FilterType: String, Codable, Equatable, Sendable { + case fiberglass + case pleatedBasic + case pleatedBetter + case pleatedBest + + public var label: String { + switch self { + case .fiberglass: return "Fiberglass (MERV 1-4)" + case .pleatedBasic: return "Basic Pleated (MERV 5-8)" + case .pleatedBetter: return "Better Pleated (MERV 9-12)" + case .pleatedBest: return "Best Pleated (MERV 13-16)" + } + } + } +} diff --git a/Sources/Routes/Models/RoomPressure.swift b/Sources/Routes/Models/RoomPressure.swift new file mode 100644 index 0000000..de451d2 --- /dev/null +++ b/Sources/Routes/Models/RoomPressure.swift @@ -0,0 +1,126 @@ +public enum RoomPressure { + + public enum Mode: String, CaseIterable, Codable, Equatable, Sendable { + case knownAirflow + case measuredPressure + + public var label: String { + switch self { + case .knownAirflow: return "Known Airflow" + case .measuredPressure: return "Measured Pressure" + } + } + } + + public enum Request: Codable, Equatable, Sendable { + case knownAirflow(KnownAirflow) + case measuredPressure(MeasuredPressure) + + public struct KnownAirflow: Codable, Equatable, Sendable { + + public let targetRoomPressure: Double + public let doorWidth: Double + public let doorHeight: Double + public let doorUndercut: Double + public let supplyAirflow: Double + public let preferredGrilleHeight: CommonReturnGrilleHeight + + public init( + targetRoomPressure: Double, + doorWidth: Double, + doorHeight: Double, + doorUndercut: Double, + supplyAirflow: Double, + preferredGrilleHeight: RoomPressure.CommonReturnGrilleHeight + ) { + self.targetRoomPressure = targetRoomPressure + self.doorWidth = doorWidth + self.doorHeight = doorHeight + self.doorUndercut = doorUndercut + self.supplyAirflow = supplyAirflow + self.preferredGrilleHeight = preferredGrilleHeight + } + } + + public struct MeasuredPressure: Codable, Equatable, Sendable { + + public let measuredRoomPressure: Double // pascals. + public let doorWidth: Double + public let doorHeight: Double + public let doorUndercut: Double + public let preferredGrilleHeight: CommonReturnGrilleHeight + + public init( + measuredRoomPressure: Double, + doorWidth: Double, + doorHeight: Double, + doorUndercut: Double, + preferredGrilleHeight: RoomPressure.CommonReturnGrilleHeight + ) { + self.measuredRoomPressure = measuredRoomPressure + self.doorWidth = doorWidth + self.doorHeight = doorHeight + self.doorUndercut = doorUndercut + self.preferredGrilleHeight = preferredGrilleHeight + } + } + } + + public struct Response: Codable, Equatable, Sendable { + + public let grilleSize: GrilleSize + public let ductSize: DuctSize + public let calculatedAirflow: Double? + public let warnings: [String] + + public init( + grilleSize: RoomPressure.Response.GrilleSize, + ductSize: RoomPressure.Response.DuctSize, + calculatedAirflow: Double? = nil, + warnings: [String] + ) { + self.grilleSize = grilleSize + self.ductSize = ductSize + self.calculatedAirflow = calculatedAirflow + self.warnings = warnings + } + + public struct DuctSize: Codable, Equatable, Sendable { + + public let diameter: Double + public let velocity: Double + + public init(diameter: Double, velocity: Double) { + self.diameter = diameter + self.velocity = velocity + } + + } + + public struct GrilleSize: Codable, Equatable, Sendable { + + public let width: Double + public let height: Double + public let area: Double + + public init(width: Double, height: Double, area: Double) { + self.width = width + self.height = height + self.area = area + } + } + + } + + public enum CommonReturnGrilleHeight: Int, CaseIterable, Codable, Equatable, Sendable { + case four = 4 + case six = 6 + case eight = 8 + case ten = 10 + case twelve = 12 + case fourteen = 14 + case twenty = 20 + + public var label: String { "\(rawValue)\"" } + } +} diff --git a/Sources/Routes/SiteRoutes.swift b/Sources/Routes/SiteRoutes.swift index 616404b..6174624 100644 --- a/Sources/Routes/SiteRoutes.swift +++ b/Sources/Routes/SiteRoutes.swift @@ -56,6 +56,7 @@ public extension SiteRoute { case dehumidifierSize(DehumidifierSize) case hvacSystemPerformance(HVACSystemPerformance) case moldRisk(MoldRisk) + case roomPressure(RoomPressure) public static let router = OneOf { Route(.case(Self.index)) { @@ -70,6 +71,9 @@ public extension SiteRoute { Route(.case(Self.moldRisk)) { MoldRisk.router } + Route(.case(Self.roomPressure)) { + RoomPressure.router + } } public enum DehumidifierSize: Equatable, Sendable { @@ -157,5 +161,23 @@ public extension SiteRoute { } } } + + public enum RoomPressure: Equatable, Sendable { + case index(mode: Routes.RoomPressure.Mode? = nil) + + public static var index: Self { .index() } + + static let rootPath = "room-pressure" + + public static let router = OneOf { + Route(.case(Self.index)) { + Path { rootPath } + Method.get + Query { + Optionally { Field("form") { Routes.RoomPressure.Mode.parser() } } + } + } + } + } } } diff --git a/Sources/Styleguide/Buttons.swift b/Sources/Styleguide/Buttons.swift index a15b3f9..1d0be7f 100644 --- a/Sources/Styleguide/Buttons.swift +++ b/Sources/Styleguide/Buttons.swift @@ -1,5 +1,41 @@ import Elementary +public struct PrimaryButton: HTML, Sendable { + let label: String + + public init(label: String) { + self.label = label + } + + public var content: some HTML { + button( + .class(""" + font-bold py-2 px-4 transition-colors + bg-blue-500 enabled:hover:bg-blue-600 + text-yellow-300 + """) + ) { label } + } +} + +public struct SecondaryButton: HTML, Sendable { + let label: String + + public init(label: String) { + self.label = label + } + + public var content: some HTML { + button( + .class(""" + font-bold py-2 px-4 transition-colors + bg-yellow-300 enabled:hover:bg-yellow-400 + text-blue-600 + """) + ) { label } + } +} + public struct SubmitButton: HTML, Sendable { let label: String diff --git a/Sources/ViewController/Extensions/HTMLAttributeExtensions.swift b/Sources/ViewController/Extensions/HTMLAttributeExtensions.swift index d204d0e..c5eac67 100644 --- a/Sources/ViewController/Extensions/HTMLAttributeExtensions.swift +++ b/Sources/ViewController/Extensions/HTMLAttributeExtensions.swift @@ -2,6 +2,10 @@ import Elementary extension HTMLAttribute where Tag == HTMLTag.input { + static func max(_ value: String) -> Self { + .init(name: "max", value: value) + } + static func min(_ value: String) -> Self { .init(name: "min", value: value) } diff --git a/Sources/ViewController/Live.swift b/Sources/ViewController/Live.swift index 3cafc4f..85f684e 100644 --- a/Sources/ViewController/Live.swift +++ b/Sources/ViewController/Live.swift @@ -81,7 +81,8 @@ extension ViewController: DependencyKey { case let .hvacSystemPerformance(route): switch route { case .index: - return request.respond(HVACSystemPerformanceForm()) + // let response = try await HVACSystemPerformance.Response.mock() + return request.respond(HVACSystemPerformanceForm(response: nil)) case let .submit(hvacRequest): let response = try await hvacRequest.respond(logger: request.logger) return HVACSystemPerformanceResult(response: response) @@ -95,6 +96,12 @@ extension ViewController: DependencyKey { let response = try await psychrometricClient.respond(request) return MoldRiskResponse(response: response) } + + case let .roomPressure(route): + switch route { + case let .index(mode): + return request.respond(RoomPressureForm(mode: mode, response: nil)) + } } }) } diff --git a/Sources/ViewController/Views/HVACSystemPerformance.swift b/Sources/ViewController/Views/HVACSystemPerformance.swift index c668a99..acb4b00 100644 --- a/Sources/ViewController/Views/HVACSystemPerformance.swift +++ b/Sources/ViewController/Views/HVACSystemPerformance.swift @@ -153,6 +153,15 @@ struct HVACSystemPerformanceResult: HTML, Sendable { LabeledMetric(label: "Sensible Heat Ratio", value: shr, units: "%") } } + + if shr < 0.7 { + div(.class("mx-8 mb-4 mt-8 p-4 rounded-xl shadow-lg border border-amber-500 bg-amber-200 text-amber-500")) { + h4(.class("text-lg font-semibold")) { "Warning:" } + p(.class("text-sm")) { + "Low sensible heat ratio may indicate excessive dehumidification or low airflow." + } + } + } } } diff --git a/Sources/ViewController/Views/MainPage.swift b/Sources/ViewController/Views/MainPage.swift index 2e3f523..8f4415f 100644 --- a/Sources/ViewController/Views/MainPage.swift +++ b/Sources/ViewController/Views/MainPage.swift @@ -85,6 +85,14 @@ private struct Header: HTML { "HVAC-System-Performance" } } + li { + a( + .class("hover:border-b \(border: .yellow)"), + .hx.get(route: .roomPressure(.index)), .hx.target("#content"), .hx.pushURL(true) + ) { + "Room-Pressure" + } + } } } } diff --git a/Sources/ViewController/Views/MoldRisk.swift b/Sources/ViewController/Views/MoldRisk.swift index f6e6435..9c36008 100644 --- a/Sources/ViewController/Views/MoldRisk.swift +++ b/Sources/ViewController/Views/MoldRisk.swift @@ -85,8 +85,8 @@ struct MoldRiskResponse: HTML { } // Display psychrometric properties. - PsychrometricPropertiesGrid(properties: response.psychrometricProperties) - .attributes(.class("mx-6")) + Properties(properties: response.psychrometricProperties) + .attributes(.class("mt-8")) // Disclaimer. Note { @@ -98,6 +98,20 @@ struct MoldRiskResponse: HTML { } } } + + private struct Properties: HTML, Sendable { + let properties: PsychrometricProperties + + var content: some HTML { + div(.class("w-full rounded-lg border")) { + h3(.class("flex justify-center text-xl font-semibold mb-6 mt-2")) { "Psychrometric Properties" } + PsychrometricPropertiesView(properties: properties) + .attributes(.class(""" + grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-6 gap-y-2 px-4 pb-4 + """)) + } + } + } } private extension MoldRisk.RiskLevel { @@ -129,46 +143,3 @@ private extension MoldRisk.RiskLevel { } } - -// TODO: Remove and use PsychrometricPropertiesView as the base. -struct PsychrometricPropertiesGrid: HTML { - let properties: PsychrometricProperties - - var content: some HTML { - div(.class("pt-8")) { - p(.class("text-xl font-semibold border-b")) { - "Psychrometric Properties:" - } - div(.class("w-full mt-6 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4")) { - displayProperty("Dew Point", \.dewPoint.rawValue) - displayProperty("Wet Bulb", \.wetBulb.rawValue) - displayProperty("Enthalpy", \.enthalpy.rawValue) - displayProperty("Density", \.density.rawValue) - displayProperty("Vapor Pressure", \.vaporPressure.rawValue) - displayProperty("Specific Volume", properties.specificVolume.rawValue) - displayProperty("Absolute Humidity", \.absoluteHumidity) - displayProperty("Humidity Ratio", properties.humidityRatio.value) - displayProperty("Degree of Saturation", properties.degreeOfSaturation.value) - } - } - } - - private func displayProperty(_ label: String, _ value: Double, _ symbol: String? = nil) -> some HTML { - let symbol = "\(symbol == nil ? "" : " \(symbol!)")" - - return p(.class("text-blue-500 dark:text-slate-200")) { - span(.class("font-semibold")) { "\(label): " } - span(.class("font-light")) { - "\(double: value)\(symbol)" - } - } - } - - private func displayProperty( - _ label: String, - _ keyPath: KeyPath - ) -> some HTML where N.Number == Double, N.Units: RawRepresentable, N.Units.RawValue == String { - let property = properties[keyPath: keyPath] - return displayProperty(label, property.rawValue, property.units.rawValue) - } -} diff --git a/Sources/ViewController/Views/RoomPressure.swift b/Sources/ViewController/Views/RoomPressure.swift new file mode 100644 index 0000000..928bce4 --- /dev/null +++ b/Sources/ViewController/Views/RoomPressure.swift @@ -0,0 +1,166 @@ +import Elementary +import ElementaryHTMX +import Routes +import Styleguide + +struct RoomPressureForm: HTML, Sendable { + let response: RoomPressure.Response? + let mode: RoomPressure.Mode + + init(mode: RoomPressure.Mode? = nil, response: RoomPressure.Response? = nil) { + self.mode = mode ?? .knownAirflow + self.response = response + } + + var content: some HTML { + div(.class("relative")) { + FormHeader(label: "Room Pressure Calculator", svg: .leftRightArrow) + + // Mode toggle / buttons. + div(.class("absolute top-0 right-0 flex items-center gap-x-0")) { + switch mode { + case .knownAirflow: + SecondaryButton(label: "Known Airflow") + .attributes(.class("rounded-s-lg")) + .attributes(.disabled, when: mode == .knownAirflow) + .attributes( + .hx.get(route: .roomPressure(.index(mode: .knownAirflow))), + .hx.target("#content"), + when: mode == .measuredPressure + ) + PrimaryButton(label: "Measured Pressure") + .attributes(.class("rounded-e-lg")) + .attributes( + .hx.get(route: .roomPressure(.index(mode: .measuredPressure))), + .hx.target("#content"), + when: mode == .knownAirflow + ) + case .measuredPressure: + PrimaryButton(label: "Known Airflow") + .attributes(.class("rounded-s-lg")) + .attributes( + .hx.get(route: .roomPressure(.index(mode: .knownAirflow))), + .hx.target("#content"), + when: mode == .measuredPressure + ) + SecondaryButton(label: "Measured Pressure") + .attributes(.class("rounded-e-lg")) + .attributes(.disabled, when: mode == .measuredPressure) + .attributes( + .hx.get(route: .roomPressure(.index(mode: .measuredPressure))), + .hx.target("#content"), + when: mode == .knownAirflow + ) + } + } + + Form(mode: mode) + + div(.id("result")) { + if let response { + RoomPressureResult(response: response) + } + } + } + } + + struct Form: HTML, Sendable { + let mode: RoomPressure.Mode + + var content: some HTML { + form { + div(.class("space-y-6")) { + LabeledContent(label: pressureLabel) { + // NB: using .attributes(..., when:...) not working, so using a switch statement. + switch mode { + case .knownAirflow: + Input(id: pressureID, placeholder: pressurePlaceholder) + .attributes(.type(.number), .step("0.1"), .min("0.1"), .max("3.0"), .autofocus, .required) + case .measuredPressure: + Input(id: pressureID, placeholder: pressurePlaceholder) + .attributes(.type(.number), .step("0.1"), .min("0.1"), .autofocus, .required) + } + } + + DoorDetails() + + if mode == .knownAirflow { + LabeledContent(label: "Supply Airflow (CFM)") { + Input(id: "supplyAirflow", placeholder: "Airflow") + .attributes(.type(.number), .step("0.1"), .min("0.1"), .required) + } + } + + PreferredGrilleHeight() + + div { + SubmitButton(label: "Calculate Return Path Size") + } + } + } + } + + private var pressureLabel: String { + switch mode { + case .knownAirflow: return "Target Room Pressure (Pascals)" + case .measuredPressure: return "Measured Room Pressure (Pascals)" + } + } + + private var pressureID: String { + switch mode { + case .knownAirflow: return "targetRoomPressure" + case .measuredPressure: return "measuredRoomPressure" + } + } + + private var pressurePlaceholder: String { + switch mode { + case .knownAirflow: return "Room pressure (max 3 pa.)" + case .measuredPressure: return "Measure pressure" + } + } + } + + private struct DoorDetails: HTML, Sendable { + + var content: some HTML { + div(.class("grid grid-cols-1 lg:grid-cols-2 gap-6")) { + LabeledContent(label: "Door Width (in.)") { + Input(id: "doorWidth", placeholder: "Width") + .attributes(.type(.number), .step("0.1"), .min("0.1"), .required) + } + + LabeledContent(label: "Door Height (in.)") { + Input(id: "doorHeight", placeholder: "Height") + .attributes(.type(.number), .step("0.1"), .min("0.1"), .required) + } + } + + LabeledContent(label: "Door Undercut (in.)") { + Input(id: "doorUndercut", placeholder: "Undercut height") + .attributes(.type(.number), .step("0.1"), .min("0.1"), .required) + } + } + } + + private struct PreferredGrilleHeight: HTML, Sendable { + + var content: some HTML { + InputLabel(for: "preferredGrilleHeight") { "Preferred Grille Height" } + select( + .id("preferredGrilleHeight"), + .name("preferredGrilleHeight"), + .class("w-full px-4 py-2 rounded-md border") + ) { + for height in RoomPressure.CommonReturnGrilleHeight.allCases { + option(.value("\(height.rawValue)")) { height.label } + } + } + } + } +} + +struct RoomPressureResult: HTML, Sendable { + let response: RoomPressure.Response +} diff --git a/docker-compose.yml b/docker-compose.yml index ab77939..e40130b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,7 +15,7 @@ x-shared_environment: &shared_environment LOG_LEVEL: ${LOG_LEVEL:-debug} - + services: app: image: swift-hvac-toolbox:latest @@ -24,6 +24,6 @@ services: environment: <<: *shared_environment ports: - - '8080:8080' + - '8081:8080' # user: '0' # uncomment to run as root for testing purposes even though Dockerfile defines 'vapor' user. command: ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"] diff --git a/justfile b/justfile index 774ec77..f44b1f2 100644 --- a/justfile +++ b/justfile @@ -1,3 +1,8 @@ +docker_image := "hvac-toolbox" +docker_tag := "latest" + +build-docker: + @docker build -t {{docker_image}}:{{docker_tag}} . run: #!/usr/bin/env zsh