WIP: Changes main page to not include sidebar, that moves to project view.

This commit is contained in:
2026-01-03 16:24:53 -05:00
parent 1d155546ae
commit 1aeb6144d5
15 changed files with 383 additions and 236 deletions

View File

@@ -10,6 +10,7 @@ extension DatabaseClient {
public var create: @Sendable (Room.Create) async throws -> Room public var create: @Sendable (Room.Create) async throws -> Room
public var delete: @Sendable (Room.ID) async throws -> Void public var delete: @Sendable (Room.ID) async throws -> Void
public var get: @Sendable (Room.ID) async throws -> Room? public var get: @Sendable (Room.ID) async throws -> Room?
public var fetch: @Sendable (Project.ID) async throws -> [Room]
} }
} }
@@ -33,6 +34,13 @@ extension DatabaseClient.Rooms {
}, },
get: { id in get: { id in
try await RoomModel.find(id, on: database).map { try $0.toDTO() } try await RoomModel.find(id, on: database).map { try $0.toDTO() }
},
fetch: { projectID in
try await RoomModel.query(on: database)
.with(\.$project)
.filter(\.$project.$id, .equal, projectID)
.all()
.map { try $0.toDTO() }
} }
) )
} }
@@ -83,6 +91,8 @@ extension Room {
.field("coolingTotal", .double, .required) .field("coolingTotal", .double, .required)
.field("coolingSensible", .double, .required) .field("coolingSensible", .double, .required)
.field("registerCount", .int8, .required) .field("registerCount", .int8, .required)
.field("createdAt", .datetime)
.field("updatedAt", .datetime)
.field("projectID", .uuid, .required, .references(ProjectModel.schema, "id")) .field("projectID", .uuid, .required, .references(ProjectModel.schema, "id"))
.unique(on: "projectID", "name") .unique(on: "projectID", "name")
.create() .create()

View File

@@ -44,6 +44,7 @@ extension SiteRoute {
extension SiteRoute.View { extension SiteRoute.View {
public enum ProjectRoute: Equatable, Sendable { public enum ProjectRoute: Equatable, Sendable {
case create(Project.Create) case create(Project.Create)
case detail(Project.ID)
case form(dismiss: Bool = false) case form(dismiss: Bool = false)
case index case index
case page(page: Int = 1, limit: Int = 25) case page(page: Int = 1, limit: Int = 25)
@@ -65,6 +66,13 @@ extension SiteRoute.View {
.map(.memberwise(Project.Create.init)) .map(.memberwise(Project.Create.init))
} }
} }
Route(.case(Self.detail)) {
Path {
rootPath
Project.ID.parser()
}
Method.get
}
Route(.case(Self.form)) { Route(.case(Self.form)) {
Path { Path {
rootPath rootPath
@@ -97,7 +105,7 @@ extension SiteRoute.View {
extension SiteRoute.View { extension SiteRoute.View {
public enum RoomRoute: Equatable, Sendable { public enum RoomRoute: Equatable, Sendable {
case form(dismiss: Bool = false) case form(dismiss: Bool = false)
case index case index(Project.ID)
static let rootPath = "rooms" static let rootPath = "rooms"
@@ -115,6 +123,9 @@ extension SiteRoute.View {
Route(.case(Self.index)) { Route(.case(Self.index)) {
Path { rootPath } Path { rootPath }
Method.get Method.get
Query {
Field("projectID") { Project.ID.parser() }
}
} }
} }
} }

View File

@@ -0,0 +1,20 @@
import Elementary
import Foundation
public struct DateView: HTML, Sendable {
let date: Date
var formatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateStyle = .short
return formatter
}
public init(_ date: Date) {
self.date = date
}
public var body: some HTML<HTMLTag.span> {
span { formatter.string(from: date) }
}
}

View File

@@ -7,3 +7,10 @@ extension HTMLAttribute where Tag: HTMLTrait.Attributes.href {
href(SiteRoute.View.router.path(for: route)) href(SiteRoute.View.router.path(for: route))
} }
} }
extension HTMLAttribute where Tag == HTMLTag.form {
public static func action(route: SiteRoute.View) -> Self {
action(SiteRoute.View.router.path(for: route))
}
}

View File

@@ -0,0 +1,28 @@
import DatabaseClient
import Fluent
import ManualDCore
import Vapor
extension DatabaseClient.Projects {
func fetchPage(
userID: User.ID,
page: Int = 1,
limit: Int = 25
) async throws -> Page<Project> {
try await fetch(userID, .init(page: page, per: limit))
}
func fetchPage(
userID: User.ID,
page: PageRequest
) async throws -> Page<Project> {
try await fetch(userID, page)
}
}
extension PageRequest {
static func next<T>(_ currentPage: Page<T>) -> Self {
.init(page: currentPage.metadata.page + 1, per: currentPage.metadata.per)
}
}

View File

@@ -46,9 +46,6 @@ extension ViewController {
self.currentUser = currentUser self.currentUser = currentUser
} }
func authenticate(_ user: User) {
self.authenticateUser(user)
}
} }
} }
@@ -62,3 +59,30 @@ extension ViewController: DependencyKey {
} }
) )
} }
extension ViewController.Request {
func authenticate(
_ login: User.Login
) async throws -> User {
@Dependency(\.database.users) var users
let token = try await users.login(login)
let user = try await users.get(token.userID)!
authenticateUser(user)
logger.debug("Logged in user: \(user.id)")
return user
}
func createAndAuthenticate(
_ signup: User.Create
) async throws -> User {
@Dependency(\.database.users) var users
let user = try await users.create(signup)
let _ = try await users.login(
.init(email: signup.email, password: signup.password)
)
authenticateUser(user)
logger.debug("Created and logged in user: \(user.id)")
return user
}
}

View File

@@ -14,24 +14,34 @@ extension ViewController.Request {
case .login(let route): case .login(let route):
switch route { switch route {
case .index: case .index:
return try await _render(isHtmxRequest: isHtmxRequest, showSidebar: false) { return view {
LoginForm() LoginForm()
} }
case .submit(let login): case .submit(let login):
let token = try await database.users.login(login) let user = try await authenticate(login)
let user = try await database.users.get(token.userID)!
authenticate(user)
let projects = try await database.projects.fetch(user.id, .init(page: 1, per: 25)) let projects = try await database.projects.fetch(user.id, .init(page: 1, per: 25))
return try await _render(isHtmxRequest: isHtmxRequest, showSidebar: false) { return view {
ProjectsTable(userID: user.id, projects: projects) ProjectsTable(userID: user.id, projects: projects)
} }
} }
case .signup(let route): case .signup(let route):
return try await route.renderView(isHtmxRequest: isHtmxRequest) switch route {
case .index:
return view {
LoginForm(style: .signup)
}
case .submit(let request):
// Create a new user and log them in.
let user = try await createAndAuthenticate(request)
let projects = try await database.projects.fetch(user.id, .init(page: 1, per: 25))
return view {
ProjectsTable(userID: user.id, projects: projects)
}
}
case .project(let route): case .project(let route):
return try await route.renderView(isHtmxRequest: isHtmxRequest) return try await route.renderView(on: self)
case .room(let route): case .room(let route):
return try await route.renderView(isHtmxRequest: isHtmxRequest) return try await route.renderView(on: self)
case .frictionRate(let route): case .frictionRate(let route):
return try await route.renderView(isHtmxRequest: isHtmxRequest) return try await route.renderView(isHtmxRequest: isHtmxRequest)
case .effectiveLength(let route): case .effectiveLength(let route):
@@ -40,54 +50,84 @@ extension ViewController.Request {
// return try await route.renderView(isHtmxRequest: isHtmxRequest) // return try await route.renderView(isHtmxRequest: isHtmxRequest)
default: default:
// FIX: FIX // FIX: FIX
return try await _render(isHtmxRequest: false) { return _render(isHtmxRequest: false) {
div { "Fix me!" } div { "Fix me!" }
} }
} }
} }
func view<C: HTML>(
@HTMLBuilder inner: () -> C
) -> AnySendableHTML where C: Sendable {
_render(isHtmxRequest: isHtmxRequest, showSidebar: showSidebar) {
inner()
}
}
var showSidebar: Bool {
switch route {
case .login, .signup, .project(.page):
return false
default:
return true
}
}
} }
extension SiteRoute.View.ProjectRoute { extension SiteRoute.View.ProjectRoute {
private var shouldShowSidebar: Bool { func renderView(on request: ViewController.Request) async throws -> AnySendableHTML {
switch self { @Dependency(\.database) var database
case .index, .page: return false let user = try request.currentUser()
default: return true
}
}
func renderView(isHtmxRequest: Bool) async throws -> AnySendableHTML {
@Dependency(\.database.projects) var projects
return try await _render(
isHtmxRequest: isHtmxRequest,
showSidebar: shouldShowSidebar
) {
switch self { switch self {
case .index: case .index:
// ProjectView(project: .mock) let projects = try await database.projects.fetchPage(userID: user.id)
let page = try await projects.fetch(UUID(0), .init(page: 1, per: 25)) return request.view {
ProjectsTable(userID: UUID(0), projects: page) ProjectsTable(userID: user.id, projects: projects)
}
case .page(let page, let limit): case .page(let page, let limit):
let page = try await projects.fetch(UUID(0), .init(page: page, per: limit)) let projects = try await database.projects.fetchPage(
ProjectsTable.Rows(projects: page) userID: user.id, page: page, limit: limit)
return ProjectsTable(userID: user.id, projects: projects)
case .form(let dismiss): case .form(let dismiss):
ProjectForm(dismiss: dismiss) return ProjectForm(dismiss: dismiss)
case .create:
div { "Fix me!" } case .create(let form):
let project = try await database.projects.create(user.id, form)
return request.view {
ProjectView(projectID: project.id, activeTab: .projects) {
ProjectDetail(project: project)
} }
} }
case .detail(let projectID):
let project = try await database.projects.get(projectID)!
return request.view {
ProjectView(projectID: projectID, activeTab: .projects) {
ProjectDetail(project: project)
}
}
}
} }
} }
extension SiteRoute.View.RoomRoute { extension SiteRoute.View.RoomRoute {
func renderView(isHtmxRequest: Bool) async throws -> AnySendableHTML { func renderView(on request: ViewController.Request) async throws -> AnySendableHTML {
@Dependency(\.database) var database
switch self { switch self {
case .form(let dismiss): case .form(let dismiss):
return RoomForm(dismiss: dismiss) return RoomForm(dismiss: dismiss)
case .index: case .index(let projectID):
return try await _render(isHtmxRequest: isHtmxRequest, active: .rooms) { let rooms = try await database.rooms.fetch(projectID)
RoomsView(rooms: Room.mocks) return request.view {
ProjectView(projectID: projectID, activeTab: .rooms) {
RoomsView(rooms: rooms)
}
} }
} }
} }
@@ -97,7 +137,7 @@ extension SiteRoute.View.FrictionRateRoute {
func renderView(isHtmxRequest: Bool) async throws -> AnySendableHTML { func renderView(isHtmxRequest: Bool) async throws -> AnySendableHTML {
switch self { switch self {
case .index: case .index:
return try await _render(isHtmxRequest: isHtmxRequest, active: .frictionRate) { return _render(isHtmxRequest: isHtmxRequest, active: .frictionRate) {
FrictionRateView() FrictionRateView()
} }
case .form(let type, let dismiss): case .form(let type, let dismiss):
@@ -128,7 +168,7 @@ extension SiteRoute.View.EffectiveLengthRoute {
func renderView(isHtmxRequest: Bool) async throws -> AnySendableHTML { func renderView(isHtmxRequest: Bool) async throws -> AnySendableHTML {
switch self { switch self {
case .index: case .index:
return try await _render(isHtmxRequest: isHtmxRequest, active: .effectiveLength) { return _render(isHtmxRequest: isHtmxRequest, active: .effectiveLength) {
EffectiveLengthsView(effectiveLengths: EffectiveLength.mocks) EffectiveLengthsView(effectiveLengths: EffectiveLength.mocks)
} }
case .form(let dismiss): case .form(let dismiss):
@@ -145,52 +185,6 @@ extension SiteRoute.View.EffectiveLengthRoute {
} }
} }
extension SiteRoute.View.SignupRoute {
func renderView(isHtmxRequest: Bool) async throws -> AnySendableHTML {
@Dependency(\.database.users) var users
switch self {
case .index:
return try await _render(isHtmxRequest: isHtmxRequest, showSidebar: false) {
LoginForm(style: .signup)
}
case .submit(let request):
_ = try await users.create(request)
// FIX: We should just login the new user at this point.
return try await _render(isHtmxRequest: isHtmxRequest, showSidebar: false) {
LoginForm()
}
// default:
// return div { "Fix Me!" }
}
}
}
// extension SiteRoute.View.LoginRoute {
// func renderView(on req: ViewController.Request) async throws -> AnySendableHTML {
//
// @Dependency(\.database) var database
//
// return try await _render(isHtmxRequest: req.isHtmxRequest, showSidebar: false) {
// switch self {
// case .index:
// LoginForm()
// case .submit(let login):
// // FIX:
// // div { "Logged in Success! Fix me!" }
// let token = try await database.users.login(login)
// let user = try await database.users.get(token.userID)!
// _ = req.authenticate(user)
// // req.authenticate(user)
// let page = try await database.projects.fetch(user.id, .init(page: 1, per: 25))
// ProjectsTable(userID: user.id, projects: page)
// }
// }
// }
// }
private func _render<C: HTML>( private func _render<C: HTML>(
isHtmxRequest: Bool, isHtmxRequest: Bool,
active activeTab: Sidebar.ActiveTab = .projects, active activeTab: Sidebar.ActiveTab = .projects,
@@ -201,10 +195,18 @@ private func _render<C: HTML>(
if isHtmxRequest { if isHtmxRequest {
return inner return inner
} }
return MainPage( return MainPage { inner }
active: activeTab, }
showSidebar: showSidebar
) { private func _render<C: HTML>(
inner isHtmxRequest: Bool,
} active activeTab: Sidebar.ActiveTab = .projects,
showSidebar: Bool = true,
@HTMLBuilder inner: () -> C
) -> AnySendableHTML where C: Sendable {
let inner = inner()
if isHtmxRequest {
return inner
}
return MainPage { inner }
} }

View File

@@ -1,19 +1,21 @@
import Elementary import Elementary
public struct MainPage<Inner: HTML>: SendableHTMLDocument where Inner: Sendable { public struct MainPage<Inner: HTML>: SendableHTMLDocument where Inner: Sendable {
public var title: String { "Manual-D" } public var title: String { "Manual-D" }
public var lang: String { "en" } public var lang: String { "en" }
let inner: Inner let inner: Inner
let activeTab: Sidebar.ActiveTab // let activeTab: Sidebar.ActiveTab
let showSidebar: Bool // let showSidebar: Bool
init( init(
active activeTab: Sidebar.ActiveTab, // active activeTab: Sidebar.ActiveTab,
showSidebar: Bool = true, // showSidebar: Bool = true,
_ inner: () -> Inner _ inner: () -> Inner
) { ) {
self.activeTab = activeTab // self.activeTab = activeTab
self.showSidebar = showSidebar // self.showSidebar = showSidebar
self.inner = inner() self.inner = inner()
} }
@@ -27,17 +29,9 @@ public struct MainPage<Inner: HTML>: SendableHTMLDocument where Inner: Sendable
} }
public var body: some HTML { public var body: some HTML {
// div(.class("bg-white dark:bg-gray-800 dark:text-white")) {
div { div {
div(.class("flex flex-row")) {
if showSidebar {
Sidebar(active: activeTab)
}
main(.class("flex flex-col h-screen w-full px-6 py-10")) {
inner inner
} }
}
}
script(.src("https://unpkg.com/lucide@latest")) {} script(.src("https://unpkg.com/lucide@latest")) {}
script { script {
"lucide.createIcons();" "lucide.createIcons();"

View File

@@ -0,0 +1,59 @@
import Elementary
import ElementaryHTMX
import ManualDCore
import Styleguide
struct ProjectDetail: HTML, Sendable {
let project: Project
var body: some HTML {
div(
.class(
"""
border border-gray-200 rounded-lg shadow-lg space-y-4 p-4 m-4
"""
)
) {
Row {
h1(.class("text-2xl font-bold")) { "Project" }
EditButton()
.attributes(
.hx.get(route: .project(.form(dismiss: false))),
.hx.target("#projectForm"),
.hx.swap(.outerHTML)
)
}
Row {
Label("Name")
span { project.name }
}
.attributes(.class("border-b border-gray-200"))
Row {
Label("Address")
span { project.streetAddress }
}
.attributes(.class("border-b border-gray-200"))
Row {
Label("City")
span { project.city }
}
.attributes(.class("border-b border-gray-200"))
Row {
Label("State")
span { project.state }
}
.attributes(.class("border-b border-gray-200"))
Row {
Label("Zip")
span { project.zipCode }
}
}
div(.id("projectForm")) {}
}
}

View File

@@ -19,7 +19,11 @@ struct ProjectForm: HTML, Sendable {
var body: some HTML { var body: some HTML {
ModalForm(id: "projectForm", dismiss: dismiss) { ModalForm(id: "projectForm", dismiss: dismiss) {
h1(.class("text-3xl font-bold pb-6 ps-2")) { "Project" } h1(.class("text-3xl font-bold pb-6 ps-2")) { "Project" }
form(.class("space-y-4 p-4")) { form(
.class("space-y-4 p-4"),
.method(.post),
.action(route: .project(.index))
) {
div { div {
label(.for("name")) { "Name" } label(.for("name")) { "Name" }
Input(id: "name", placeholder: "Name") Input(id: "name", placeholder: "Name")

View File

@@ -3,57 +3,113 @@ import ElementaryHTMX
import ManualDCore import ManualDCore
import Styleguide import Styleguide
struct ProjectView: HTML, Sendable { struct ProjectView<Inner: HTML>: HTML, Sendable where Inner: Sendable {
let project: Project let projectID: Project.ID
let activeTab: Sidebar.ActiveTab
let inner: Inner
init(
projectID: Project.ID,
activeTab: Sidebar.ActiveTab,
@HTMLBuilder inner: () -> Inner
) {
self.projectID = projectID
self.activeTab = activeTab
self.inner = inner()
}
var body: some HTML { var body: some HTML {
div( div {
div(.class("flex flex-row")) {
Sidebar(active: activeTab, projectID: projectID)
main(.class("flex flex-col h-screen w-full px-6 py-10")) {
inner
}
}
}
}
}
// TODO: Update to use DaisyUI drawer.
struct Sidebar: HTML {
let active: ActiveTab
let projectID: Project.ID
var body: some HTML {
aside(
.class( .class(
""" """
border border-gray-200 rounded-lg shadow-lg space-y-4 p-4 m-4 h-screen sticky top-0 max-w-[280px] flex-none
border-r-2 border-gray-200
shadow-lg
""" """
) )
) { ) {
Row {
h1(.class("text-2xl font-bold")) { "Project" }
EditButton()
.attributes(
.hx.get(route: .project(.form(dismiss: false))),
.hx.target("#projectForm"),
.hx.swap(.outerHTML)
)
}
// TODO: Move somewhere outside of the sidebar.
Row { Row {
Label("Name") Label("Theme")
span { project.name } input(.type(.checkbox), .class("toggle theme-controller"), .value("light"))
} }
.attributes(.class("border-b border-gray-200")) .attributes(.class("p-4"))
Row { row(title: "Project", icon: .mapPin, route: .project(.index))
Label("Address") .attributes(.data("active", value: active == .projects ? "true" : "false"))
span { project.streetAddress }
}
.attributes(.class("border-b border-gray-200"))
Row { row(title: "Rooms", icon: .doorClosed, route: .room(.index(projectID)))
Label("City") .attributes(.data("active", value: active == .rooms ? "true" : "false"))
span { project.city }
}
.attributes(.class("border-b border-gray-200"))
Row { row(title: "Equivalent Lengths", icon: .rulerDimensionLine, route: .effectiveLength(.index))
Label("State") .attributes(.data("active", value: active == .effectiveLength ? "true" : "false"))
span { project.state }
}
.attributes(.class("border-b border-gray-200"))
Row { row(title: "Friction Rate", icon: .squareFunction, route: .frictionRate(.index))
Label("Zip") .attributes(.data("active", value: active == .frictionRate ? "true" : "false"))
span { project.zipCode }
row(title: "Duct Sizes", icon: .wind, href: "#")
.attributes(.data("active", value: active == .ductSizing ? "true" : "false"))
} }
} }
div(.id("projectForm")) {} // TODO: Use SiteRoute.View routes as href.
private func row(
title: String,
icon: Icon.Key,
href: String
) -> some HTML<HTMLTag.a> {
a(
.class(
"""
flex w-full items-center gap-4
hover:bg-gray-300 hover:text-gray-800
data-[active=true]:bg-gray-300 data-[active=true]:text-gray-800
px-4 py-2
"""
),
.href(href)
) {
Icon(icon)
span(.class("text-xl")) {
title
}
}
}
private func row(
title: String,
icon: Icon.Key,
route: SiteRoute.View
) -> some HTML<HTMLTag.a> {
row(title: title, icon: icon, href: SiteRoute.View.router.path(for: route))
}
}
extension Sidebar {
enum ActiveTab: Equatable, Sendable {
case projects
case rooms
case effectiveLength
case frictionRate
case ductSizing
} }
} }

View File

@@ -24,7 +24,10 @@ struct ProjectsTable: HTML, Sendable {
.data("tip", value: "Add project") .data("tip", value: "Add project")
) { ) {
button( button(
.class("btn btn-primary w-[40px] text-2xl") .class("btn btn-primary w-[40px] text-2xl"),
.hx.get(route: .project(.form(dismiss: false))),
.hx.target("#projectForm"),
.hx.swap(.outerHTML)
) { ) {
"+" "+"
} }
@@ -39,6 +42,7 @@ struct ProjectsTable: HTML, Sendable {
th { Label("Date") } th { Label("Date") }
th { Label("Name") } th { Label("Name") }
th { Label("Address") } th { Label("Address") }
th {}
} }
} }
tbody { tbody {
@@ -46,6 +50,8 @@ struct ProjectsTable: HTML, Sendable {
} }
} }
} }
ProjectForm(dismiss: true)
} }
} }
} }
@@ -57,9 +63,15 @@ extension ProjectsTable {
var body: some HTML { var body: some HTML {
for project in projects.items { for project in projects.items {
tr(.id("\(project.id)")) { tr(.id("\(project.id)")) {
td { "\(project.createdAt)" } td { DateView(project.createdAt) }
td { "\(project.name)" } td { "\(project.name)" }
td { "\(project.streetAddress)" } td { "\(project.streetAddress)" }
td {
a(
.class("btn btn-success"),
.href(route: .project(.detail(project.id)))
) { ">" }
}
} }
} }
// Have a row that when revealed fetches the next page, // Have a row that when revealed fetches the next page,

View File

@@ -1,86 +0,0 @@
import Elementary
import ManualDCore
import Styleguide
// TODO: Update to use DaisyUI drawer.
struct Sidebar: HTML {
let active: ActiveTab
var body: some HTML {
aside(
.class(
"""
h-screen sticky top-0 max-w-[280px] flex-none
border-r-2 border-gray-200
shadow-lg
"""
)
) {
// TODO: Move somewhere outside of the sidebar.
Row {
Label("Theme")
input(.type(.checkbox), .class("toggle theme-controller"), .value("light"))
}
.attributes(.class("p-4"))
row(title: "Project", icon: .mapPin, route: .project(.index))
.attributes(.data("active", value: active == .projects ? "true" : "false"))
row(title: "Rooms", icon: .doorClosed, route: .room(.index))
.attributes(.data("active", value: active == .rooms ? "true" : "false"))
row(title: "Equivalent Lengths", icon: .rulerDimensionLine, route: .effectiveLength(.index))
.attributes(.data("active", value: active == .effectiveLength ? "true" : "false"))
row(title: "Friction Rate", icon: .squareFunction, route: .frictionRate(.index))
.attributes(.data("active", value: active == .frictionRate ? "true" : "false"))
row(title: "Duct Sizes", icon: .wind, href: "#")
.attributes(.data("active", value: active == .ductSizing ? "true" : "false"))
}
}
// TODO: Use SiteRoute.View routes as href.
private func row(
title: String,
icon: Icon.Key,
href: String
) -> some HTML<HTMLTag.a> {
a(
.class(
"""
flex w-full items-center gap-4
hover:bg-gray-300 hover:text-gray-800
data-[active=true]:bg-gray-300 data-[active=true]:text-gray-800
px-4 py-2
"""
),
.href(href)
) {
Icon(icon)
span(.class("text-xl")) {
title
}
}
}
private func row(
title: String,
icon: Icon.Key,
route: SiteRoute.View
) -> some HTML<HTMLTag.a> {
row(title: title, icon: icon, href: SiteRoute.View.router.path(for: route))
}
}
extension Sidebar {
enum ActiveTab: Equatable, Sendable {
case projects
case rooms
case effectiveLength
case frictionRate
case ductSizing
}
}

View File

@@ -7,7 +7,7 @@ run-css:
@./tailwindcss -i input.css -o output.css --watch @./tailwindcss -i input.css -o output.css --watch
run: run:
@SWIFT_BACTRACE=enable=no swift run App @swift run App serve --log debug
build-docker: build-docker:
@podman build -f docker/Dockerfile.dev -t {{docker_image}}:dev . @podman build -f docker/Dockerfile.dev -t {{docker_image}}:dev .

View File

@@ -1325,6 +1325,12 @@
--btn-fg: var(--color-secondary-content); --btn-fg: var(--color-secondary-content);
} }
} }
.btn-success {
@layer daisyui.l1.l2.l3 {
--btn-color: var(--color-success);
--btn-fg: var(--color-success-content);
}
}
.invalid\:border-red-500 { .invalid\:border-red-500 {
&:invalid { &:invalid {
border-color: var(--color-red-500); border-color: var(--color-red-500);