feat: Renames quick calc routes / views to ductulator. Adds button to home page for using ductulator, needs added to navbar still.

This commit is contained in:
2026-02-09 16:36:24 -05:00
parent 007d13be2f
commit 06b663052e
10 changed files with 235 additions and 137 deletions

View File

@@ -8,8 +8,6 @@
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
monospace;
--color-green-400: oklch(79.2% 0.209 151.711);
--color-sky-600: oklch(58.8% 0.158 241.966);
--color-violet-600: oklch(54.1% 0.281 293.009);
--color-gray-200: oklch(92.8% 0.006 264.531);
--color-gray-400: oklch(70.7% 0.022 261.325);
--color-black: #000;
@@ -5304,6 +5302,12 @@
}
}
}
.-mx-2 {
margin-inline: calc(var(--spacing) * -2);
}
.-mx-4 {
margin-inline: calc(var(--spacing) * -4);
}
.mx-10 {
margin-inline: calc(var(--spacing) * 10);
}
@@ -5399,12 +5403,18 @@
}
}
}
.-my-4 {
margin-block: calc(var(--spacing) * -4);
}
.my-1\.5 {
margin-block: calc(var(--spacing) * 1.5);
}
.my-6 {
margin-block: calc(var(--spacing) * 6);
}
.my-8 {
margin-block: calc(var(--spacing) * 8);
}
.my-auto {
margin-block: auto;
}
@@ -5620,9 +5630,6 @@
.me-4 {
margin-inline-end: calc(var(--spacing) * 4);
}
.me-6 {
margin-inline-end: calc(var(--spacing) * 6);
}
.me-10 {
margin-inline-end: calc(var(--spacing) * 10);
}
@@ -5686,6 +5693,12 @@
.mt-10 {
margin-top: calc(var(--spacing) * 10);
}
.mt-20 {
margin-top: calc(var(--spacing) * 20);
}
.mt-30 {
margin-top: calc(var(--spacing) * 30);
}
.breadcrumbs {
@layer daisyui.l1.l2.l3 {
max-width: 100%;
@@ -6707,12 +6720,6 @@
.w-px {
width: 1px;
}
.max-w-lg {
max-width: var(--container-lg);
}
.max-w-xl {
max-width: var(--container-xl);
}
.min-w-0 {
min-width: calc(var(--spacing) * 0);
}
@@ -6990,13 +6997,6 @@
.gap-4 {
gap: calc(var(--spacing) * 4);
}
.space-y-1 {
:where(& > :not(:last-child)) {
--tw-space-y-reverse: 0;
margin-block-start: calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));
margin-block-end: calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)));
}
}
.space-y-2 {
:where(& > :not(:last-child)) {
--tw-space-y-reverse: 0;
@@ -7642,9 +7642,6 @@
}
}
}
.bg-base-100 {
background-color: var(--color-base-100);
}
.bg-base-200 {
background-color: var(--color-base-200);
}
@@ -8779,9 +8776,6 @@
color: var(--color-warning);
}
}
.text-accent {
color: var(--color-accent);
}
.text-base-content {
color: var(--color-base-content);
}
@@ -9751,16 +9745,21 @@
}
}
}
.md\:mt-6 {
@media (width >= 48rem) {
margin-top: calc(var(--spacing) * 6);
}
}
.md\:mt-15 {
@media (width >= 48rem) {
margin-top: calc(var(--spacing) * 15);
}
}
.md\:grid-cols-2 {
@media (width >= 48rem) {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
.md\:grid-cols-3 {
@media (width >= 48rem) {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
.lg\:drawer-open {
@media (width >= 64rem) {
@layer daisyui.l1.l2.l3 {
@@ -9814,9 +9813,14 @@
}
}
}
.lg\:grid-cols-4 {
.lg\:mx-20 {
@media (width >= 64rem) {
grid-template-columns: repeat(4, minmax(0, 1fr));
margin-inline: calc(var(--spacing) * 20);
}
}
.lg\:mt-6 {
@media (width >= 64rem) {
margin-top: calc(var(--spacing) * 6);
}
}
.is-drawer-close\:mx-auto {
@@ -9858,11 +9862,6 @@
overflow: visible;
}
}
.is-drawer-close\:text-green-400 {
&:where(.drawer-toggle:not(:checked) ~ .drawer-side, .drawer-toggle:not(:checked) ~ .drawer-side *) {
color: var(--color-green-400);
}
}
.is-drawer-open\:flex {
&:where(.drawer-toggle:checked ~ .drawer-side, .drawer-toggle:checked ~ .drawer-side *) {
display: flex;

View File

@@ -14,7 +14,7 @@ private let viewRouteMiddleware: [any Middleware] = [
extension SiteRoute.View {
var middleware: [any Middleware]? {
switch self {
case .home, .login, .signup, .test, .quickCalc:
case .home, .login, .signup, .test, .ductulator:
return nil
case .project, .user:
return viewRouteMiddleware

View File

@@ -12,7 +12,7 @@ extension SiteRoute {
case login(LoginRoute)
case signup(SignupRoute)
case project(ProjectRoute)
case quickCalc(QuickCalcRoute)
case ductulator(DuctulatorRoute)
case user(UserRoute)
//FIX: Remove.
case test
@@ -34,8 +34,8 @@ extension SiteRoute {
Route(.case(Self.project)) {
SiteRoute.View.ProjectRoute.router
}
Route(.case(Self.quickCalc)) {
SiteRoute.View.QuickCalcRoute.router
Route(.case(Self.ductulator)) {
SiteRoute.View.DuctulatorRoute.router
}
Route(.case(Self.user)) {
SiteRoute.View.UserRoute.router
@@ -991,7 +991,7 @@ extension SiteRoute.View.UserRoute {
}
extension SiteRoute.View {
public enum QuickCalcRoute: Equatable, Sendable {
public enum DuctulatorRoute: Equatable, Sendable {
case index
case submit(Form)

View File

@@ -1,4 +1,6 @@
import Elementary
import ElementaryHTMX
import ManualDCore
public struct SubmitButton: HTML, Sendable {
let title: String
@@ -74,3 +76,17 @@ public struct TrashButton: HTML, Sendable {
}
}
}
public struct DuctulatorButton: HTML, Sendable {
public init() {}
public var body: some HTML<HTMLTag.a> {
a(
.class("btn"),
.href(route: .ductulator(.index)),
.target(.blank)
) {
"Ductulator"
}
}
}

View File

@@ -96,7 +96,7 @@ extension ViewController.Request {
case .project(let route):
return await route.renderView(on: self)
case .quickCalc(let route):
case .ductulator(let route):
return await route.renderView(on: self)
case .user(let route):
@@ -712,7 +712,7 @@ extension SiteRoute.View.UserRoute.Profile {
}
}
extension SiteRoute.View.QuickCalcRoute {
extension SiteRoute.View.DuctulatorRoute {
func renderView(
on request: ViewController.Request
@@ -722,7 +722,7 @@ extension SiteRoute.View.QuickCalcRoute {
switch self {
case .index:
return await request.view {
QuickCalcView(
DuctulatorView(
isLoggedIn: request.isLoggedIn
)
}
@@ -736,7 +736,7 @@ extension SiteRoute.View.QuickCalcRoute {
}
return (ductSize, rectangularSize)
} onSuccess: { (ductSize, rectangularSize) in
QuickCalcView.Result(ductSize: ductSize, rectangularSize: rectangularSize)
DuctulatorView.Result(ductSize: ductSize, rectangularSize: rectangularSize)
}
}
}

View File

@@ -6,7 +6,7 @@ import ManualDClient
import ManualDCore
import Styleguide
struct QuickCalcView: HTML, Sendable {
struct DuctulatorView: HTML, Sendable {
let isLoggedIn: Bool
@@ -32,7 +32,7 @@ struct QuickCalcView: HTML, Sendable {
div(.class("flex space-x-6 items-center text-4xl")) {
SVG(.calculator)
h1(.class("text-4xl font-bold me-10")) {
"Duct Size"
"Ductulator"
}
}
@@ -42,7 +42,7 @@ struct QuickCalcView: HTML, Sendable {
form(
.class("space-y-4 mt-6"),
.hx.post(route: .quickCalc(.index)),
.hx.post(route: .ductulator(.index)),
.hx.target("#\(Result.id)"),
.hx.swap(.outerHTML)
) {
@@ -103,7 +103,7 @@ struct QuickCalcView: HTML, Sendable {
h2(.class("text-3xl font-bold")) { "Result" }
button(
.class("btn btn-primary"),
.hx.get(route: .quickCalc(.index)),
.hx.get(route: .ductulator(.index)),
.hx.target("body"),
.hx.swap(.outerHTML)
) {

View File

@@ -1,15 +1,19 @@
import Elementary
import ElementaryHTMX
import Styleguide
struct HomeView: HTML, Sendable {
var body: some HTML {
div( // Uncomment to test different theme's.
// .data("theme", value: "cyberpunk")
// NOTE: Footer background color will follow system theme, it will actually be the
// same as the `hero` background in reality.
// NOTE: Footer background color will follow system theme.
) {
div(.class("flex justify-end m-4")) {
div(.class("flex justify-end space-x-4 m-4")) {
DuctulatorButton()
.attributes(.class("btn-ghost btn-accent text-lg"))
.tooltip("Duct size calculator", position: .left)
button(
.class("btn btn-ghost btn-secondary text-lg"),
.hx.get(route: .login(.index())),
@@ -20,12 +24,13 @@ struct HomeView: HTML, Sendable {
"Login"
}
}
div(.class("hero")) {
div(.class("mx-10 lg:mx-20")) {
div(
.class(
"""
relative hero-content text-center bg-base-300
w-full min-h-[400px] rounded-3xl shadow-3xl overflow-hidden
relative text-center bg-base-300
rounded-3xl shadow-3xl overflow-hidden
"""
)
) {
@@ -62,7 +67,7 @@ struct HomeView: HTML, Sendable {
) {
"Get Started"
}
p(.class("text-xs italic mt-8")) {
p(.class("text-xs italic my-6")) {
"""
Manual-D™ is a trademark of Air Conditioning Contractors of America (ACCA).
@@ -72,46 +77,45 @@ struct HomeView: HTML, Sendable {
}
}
}
div(.class("grid grid-cols-1 md:grid-cols-2 gap-4 mx-20 my-6")) {
div(.class("border-3 border-accent rounded-lg shadow-lg p-4")) {
div(.class("flex items-center space-x-4")) {
div(.class("text-5xl text-primary font-bold")) {
"Features"
}
}
div(.class("text-xl ms-10 mt-10")) {
ul(.class("list-disc")) {
li {
div(
.class("font-bold italic bg-secondary rounded-lg shadow-lg px-4 w-fit")
) {
"Built by humans"
}
div(.class("grid grid-cols-1 md:grid-cols-2 gap-4 my-6")) {
div(.class("border-3 border-accent rounded-lg shadow-lg p-4")) {
div(.class("flex items-center space-x-4")) {
div(.class("text-5xl text-primary font-bold")) {
"Features"
}
}
div(.class("text-xl ms-10 mt-10")) {
ul(.class("list-disc")) {
li {
div(
.class("font-bold italic bg-secondary rounded-lg shadow-lg px-4 w-fit")
) {
"Built by humans"
}
}
li { "Fully open source." }
li { "Great replacement for speed sheet users." }
li { "Great for classrooms." }
li { "Store your projects in one place." }
li { "Export final project to pdf." }
li { "Import room loads via CSV file." }
li { "Web based." }
li { "Self host (run on your own infrastructure)." }
}
li { "Fully open source." }
li { "Great replacement for speed sheet users." }
li { "Great for classrooms." }
li { "Store your projects in one place." }
li { "Export final project to pdf." }
li { "Import room loads via CSV file." }
li { "Web based." }
li { "Self host (run on your own infrastructure)." }
}
}
}
div(.class("border-3 border-accent rounded-lg shadow-lg p-4")) {
div(.class("text-5xl text-primary font-bold")) {
"Coming Soon"
}
div(.class("text-xl ms-10 mt-10")) {
ul(.class("list-disc")) {
li { "API integration." }
li { "Command line interface." }
li { "Fitting selection tool." }
li { "Room load import from PDF." }
div(.class("border-3 border-accent rounded-lg shadow-lg p-4")) {
div(.class("text-5xl text-primary font-bold")) {
"Coming Soon"
}
div(.class("text-xl ms-10 mt-10")) {
ul(.class("list-disc")) {
li { "API integration." }
li { "Command line interface." }
li { "Fitting selection tool." }
li { "Room load import from PDF." }
}
}
}
}
@@ -119,23 +123,24 @@ struct HomeView: HTML, Sendable {
}
}
// TODO: When beta flag is gone, then remove the responsive margin of the header.
var header: some HTML<HTMLTag.div> {
div(.class("flex justify-center items-center")) {
div(.class("flex justify-center mt-30 md:mt-15 lg:mt-6")) {
div(
.class(
"""
flex border-b-6 border-accent
flex items-end border-b-6 border-accent
text-8xl font-bold my-auto space-2
"""
)
) {
h1(.class("me-2")) { "Duct Calc" }
div(.class("")) {
div {
span(
.class(
"""
bg-secondary rounded-md
text-5xl rotate-180 p-2
text-5xl rotate-180 p-2 -mx-2
"""
),
.style("writing-mode: vertical-rl")

View File

@@ -28,13 +28,13 @@ struct ViewControllerTests {
}
@Test
func quickCalc() async throws {
func ductulator() async throws {
try await withDependencies {
$0.viewController = .liveValue
$0.auth = .failing
} operation: {
@Dependency(\.viewController) var viewController
let view = try await viewController.view(.test(.quickCalc(.index)))
let view = try await viewController.view(.test(.ductulator(.index)))
assertSnapshot(of: view, as: .html)
}
}

View File

@@ -0,0 +1,77 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Duct Calc</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta content="ductcalc.com" name="og:site_name">
<meta content="Duct Calc" name="og:title">
<meta content="Duct sizing based on ACCA, Manual-D." name="description">
<meta content="Duct sizing based on ACCA, Manual-D." name="og:description">
<meta content="/images/mand_logo.png" name="og:image">
<meta content="/images/mand_logo.png" name="twitter:image">
<meta content="Duct Calc" name="twitter:image:alt">
<meta content="summary_large_image" name="twitter:card">
<meta content="1536" name="og:image:width">
<meta content="1024" name="og:image:height">
<meta content="duct, hvac, duct-design, duct design, manual-d, manual d, design" name="keywords">
<script src="https://unpkg.com/htmx.org@2.0.8"></script>
<script src="/js/htmx-download.js"></script>
<script src="/js/main.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<link rel="stylesheet" href="/css/output.css">
<link rel="stylesheet" href="/css/htmx.css">
<link rel="icon" href="/images/favicon.ico" type="image/x-icon">
<link rel="icon" href="/images/favicon-32x32.png" type="image/png">
<link rel="icon" href="/images/favicon-16x16.png" type="image/png">
<link rel="apple-touch-icon" sizes="180x180" href="/images/apple-touch-icon.png">
<link rel="manifest" href="/site.webmanifest">
<script src="https://unpkg.com/htmx-remove@latest" crossorigin="anonymous" integrity="sha384-NwB2Xh66PNEYfVki0ao13UAFmdNtMIdBKZ8sNGRT6hKfCPaINuZ4ScxS6vVAycPT"></script>
</head>
<body>
<div class="flex flex-col min-h-screen min-w-full justify-between" data-theme="default">
<main class="flex flex-col min-h-screen min-w-full grow mb-auto">
<div>
<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="tooltip tooltip-right" data-tip="Home">
<a class="flex w-fit h-fit text-xl items-end px-4 py-2 btn btn-square btn-ghost hover:bg-neutral hover:text-white" href="/">
<img src="/images/mand_logo_sm.webp">
Duct Calc<span></span></a>
</div>
</div>
</nav>
<div class="flex justify-center items-center px-10">
<div class="bg-base-300 rounded-3xl shadow-3xl
p-6 w-full">
<div class="flex space-x-6 items-center text-4xl">
<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-calculator-icon lucide-calculator"><rect width="16" height="20" x="4" y="2" rx="2"/><line x1="8" x2="16" y1="6" y2="6"/><line x1="16" x2="16" y1="14" y2="18"/><path d="M16 10h.01"/><path d="M12 10h.01"/><path d="M8 10h.01"/><path d="M12 14h.01"/><path d="M8 14h.01"/><path d="M12 18h.01"/><path d="M8 18h.01"/></svg>
<h1 class="text-4xl font-bold me-10">Ductulator</h1>
</div>
<p class="text-primary font-bold italic">Calculate duct size for the given parameters</p>
<form class="space-y-4 mt-6" hx-post="/duct-size" hx-target="#resultView" hx-swap="outerHTML">
<label class="input w-full"><span class="label">CFM</span>
<input name="cfm" type="number" placeholder="1000" required autofocus>
Friction Rate</label><label class="input w-full"><span class="label"></span>
<input name="frictionRate" value="0.06" required type="number" min="0.01" step="0.01">
Height</label><label class="input w-full"><span class="label"></span>
<input name="height" type="number" placeholder="Height (Optional)"></label>
<button class="btn btn-secondary btn-block mt-6" type="submit">Submit</button>
</form>
<div id="resultView"></div>
</div>
</div>
</div>
</main>
<div class="bottom-0 left-0 bg-error">
<footer class="footer sm:footer-horizontal footer-center
bg-base-300 text-base-content p-4">
<aside>
<p>Copyright © 2026 - All rights reserved by Michael Housh</p>
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>
</aside>
</footer>
</div>
</div>
</body>
</html>

View File

@@ -32,67 +32,68 @@
<div class="flex flex-col min-h-screen min-w-full justify-between" data-theme="default">
<main class="flex flex-col min-h-screen min-w-full grow mb-auto">
<div>
<div class="flex justify-end m-4">
<div class="flex justify-end space-x-4 m-4">
<div class="tooltip tooltip-left" data-tip="Duct size calculator"><a class="btn btn-ghost btn-accent text-lg" href="/duct-size" target="_blank">Ductulator</a></div>
<button class="btn btn-ghost btn-secondary text-lg" hx-get="/login" hx-target="body" hx-swap="outerHTML" hx-push-url="true">Login</button>
</div>
<div class="hero">
<div class="relative hero-content text-center bg-base-300
w-full min-h-[400px] rounded-3xl shadow-3xl overflow-hidden">
<div class="mx-10 lg:mx-20">
<div class="relative text-center bg-base-300
rounded-3xl shadow-3xl overflow-hidden">
<div class="bg-secondary text-xl font-bold
absolute top-10 -left-15
px-6 py-2 w-[250px] -rotate-45">BETA</div>
<div>
<div class="flex justify-center items-center">
<div class="flex border-b-6 border-accent
<div class="flex justify-center mt-30 md:mt-15 lg:mt-6">
<div class="flex items-end border-b-6 border-accent
text-8xl font-bold my-auto space-2">
<h1 class="me-2">Duct Calc</h1>
<div class="">
<div>
<span class="bg-secondary rounded-md
text-5xl rotate-180 p-2" style="writing-mode: vertical-rl">Pro</span>
text-5xl rotate-180 p-2 -mx-2" style="writing-mode: vertical-rl">Pro</span>
</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>
<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>
<p class="text-xs italic mt-8">
<p class="text-xs italic my-6">
Manual-D™ is a trademark of Air Conditioning Contractors of America (ACCA).
This site is not designed by or affiliated with ACCA.
</p>
</div>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mx-20 my-6">
<div class="border-3 border-accent rounded-lg shadow-lg p-4">
<div class="flex items-center space-x-4">
<div class="text-5xl text-primary font-bold">Features</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 my-6">
<div class="border-3 border-accent rounded-lg shadow-lg p-4">
<div class="flex items-center space-x-4">
<div class="text-5xl text-primary font-bold">Features</div>
</div>
<div class="text-xl ms-10 mt-10">
<ul class="list-disc">
<li>
<div class="font-bold italic bg-secondary rounded-lg shadow-lg px-4 w-fit">Built by humans</div>
</li>
<li>Fully open source.</li>
<li>Great replacement for speed sheet users.</li>
<li>Great for classrooms.</li>
<li>Store your projects in one place.</li>
<li>Export final project to pdf.</li>
<li>Import room loads via CSV file.</li>
<li>Web based.</li>
<li>Self host (run on your own infrastructure).</li>
</ul>
</div>
</div>
<div class="text-xl ms-10 mt-10">
<ul class="list-disc">
<li>
<div class="font-bold italic bg-secondary rounded-lg shadow-lg px-4 w-fit">Built by humans</div>
</li>
<li>Fully open source.</li>
<li>Great replacement for speed sheet users.</li>
<li>Great for classrooms.</li>
<li>Store your projects in one place.</li>
<li>Export final project to pdf.</li>
<li>Import room loads via CSV file.</li>
<li>Web based.</li>
<li>Self host (run on your own infrastructure).</li>
</ul>
</div>
</div>
<div class="border-3 border-accent rounded-lg shadow-lg p-4">
<div class="text-5xl text-primary font-bold">Coming Soon</div>
<div class="text-xl ms-10 mt-10">
<ul class="list-disc">
<li>API integration.</li>
<li>Command line interface.</li>
<li>Fitting selection tool.</li>
<li>Room load import from PDF.</li>
</ul>
<div class="border-3 border-accent rounded-lg shadow-lg p-4">
<div class="text-5xl text-primary font-bold">Coming Soon</div>
<div class="text-xl ms-10 mt-10">
<ul class="list-disc">
<li>API integration.</li>
<li>Command line interface.</li>
<li>Fitting selection tool.</li>
<li>Room load import from PDF.</li>
</ul>
</div>
</div>
</div>
</div>