From 9356ccb1c92a07df25099b16f2e61f8f5136cc18 Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Thu, 8 Jan 2026 12:40:05 -0500 Subject: [PATCH] feat: Updates sidebar to use the drawer classes from daisyui, currently doesn't open automatically on large screens like I want. --- Public/css/output.css | 304 ++++++++++++++---- Sources/ApiController/Live.swift | 7 + .../App/Middleware/ViewRoute+middleware.swift | 2 +- Sources/DatabaseClient/Projects.swift | 36 +++ Sources/ManualDCore/Project.swift | 13 + Sources/ManualDCore/Routes/ApiRoute.swift | 26 ++ Sources/ManualDCore/Routes/ViewRoute.swift | 6 + Sources/Styleguide/Icon.swift | 1 + Sources/Styleguide/SVG.swift | 40 +++ Sources/ViewController/Live.swift | 4 + .../Views/Project/ProjectView.swift | 235 +++++++++----- Sources/ViewController/Views/TestPage.swift | 51 +++ 12 files changed, 578 insertions(+), 147 deletions(-) create mode 100644 Sources/ViewController/Views/TestPage.swift diff --git a/Public/css/output.css b/Public/css/output.css index 8555a75..9a60fa1 100644 --- a/Public/css/output.css +++ b/Public/css/output.css @@ -9,18 +9,15 @@ monospace; --color-red-500: oklch(63.7% 0.237 25.331); --color-red-600: oklch(57.7% 0.245 27.325); + --color-green-400: oklch(79.2% 0.209 151.711); --color-indigo-600: oklch(51.1% 0.262 276.966); --color-slate-300: oklch(86.9% 0.022 252.894); --color-slate-900: oklch(20.8% 0.042 265.755); --color-gray-200: oklch(92.8% 0.006 264.531); - --color-gray-300: oklch(87.2% 0.01 258.338); --color-gray-400: oklch(70.7% 0.022 261.325); - --color-gray-800: oklch(27.8% 0.033 256.848); --color-black: #000; --color-white: #fff; --spacing: 0.25rem; - --text-xs: 0.75rem; - --text-xs--line-height: calc(1 / 0.75); --text-sm: 0.875rem; --text-sm--line-height: calc(1.25 / 0.875); --text-lg: 1.125rem; @@ -4225,21 +4222,12 @@ --toast-y: 0; } } - .top-0 { - top: calc(var(--spacing) * 0); - } .top-2 { top: calc(var(--spacing) * 2); } .right-2 { right: calc(var(--spacing) * 2); } - .right-4 { - right: calc(var(--spacing) * 4); - } - .right-6 { - right: calc(var(--spacing) * 6); - } .dock-sm { @layer daisyui.l1.l2 { height: calc(0.25rem * 14); @@ -4297,12 +4285,6 @@ } } } - .bottom-4 { - bottom: calc(var(--spacing) * 4); - } - .bottom-6 { - bottom: calc(var(--spacing) * 6); - } .join { display: inline-flex; align-items: stretch; @@ -5289,6 +5271,9 @@ .mx-2 { margin-inline: calc(var(--spacing) * 2); } + .mx-4 { + margin-inline: calc(var(--spacing) * 4); + } .file-input-ghost { @layer daisyui.l1.l2 { background-color: transparent; @@ -5375,6 +5360,9 @@ } } } + .my-1\.5 { + margin-block: calc(var(--spacing) * 1.5); + } .my-2 { margin-block: calc(var(--spacing) * 2); } @@ -6380,6 +6368,14 @@ height: var(--size); } } + .size-4 { + width: calc(var(--spacing) * 4); + height: calc(var(--spacing) * 4); + } + .size-7 { + width: calc(var(--spacing) * 7); + height: calc(var(--spacing) * 7); + } .status-lg { @layer daisyui.l1.l2 { width: calc(0.25rem * 3); @@ -6428,6 +6424,9 @@ .h-screen { height: 100vh; } + .min-h-full { + min-height: 100%; + } .btn-wide { @layer daisyui.l1.l2 { width: 100%; @@ -6559,18 +6558,15 @@ .w-full { width: 100%; } - .max-w-\[280px\] { - max-width: 280px; - } - .flex-none { - flex: none; - } .flex-shrink { flex-shrink: 1; } .flex-grow { flex-grow: 1; } + .grow { + flex-grow: 1; + } .border-collapse { border-collapse: collapse; } @@ -6755,14 +6751,11 @@ .flex-col { flex-direction: column; } - .flex-row { - flex-direction: row; - } .flex-wrap { flex-wrap: wrap; } - .items-center { - align-items: center; + .items-start { + align-items: flex-start; } .justify-between { justify-content: space-between; @@ -7095,10 +7088,6 @@ border-style: var(--tw-border-style); border-width: 1px; } - .border-r-2 { - border-right-style: var(--tw-border-style); - border-right-width: 2px; - } .border-b { border-bottom-style: var(--tw-border-style); border-bottom-width: 1px; @@ -7375,6 +7364,12 @@ .bg-base-100 { background-color: var(--color-base-100); } + .bg-base-200 { + background-color: var(--color-base-200); + } + .bg-base-300 { + background-color: var(--color-base-300); + } .bg-red-500 { background-color: var(--color-red-500); } @@ -7666,9 +7661,6 @@ .p-4 { padding: calc(var(--spacing) * 4); } - .p-6 { - padding: calc(var(--spacing) * 6); - } .menu-title { @layer daisyui.l1.l2.l3 { padding-inline: calc(0.25rem * 3); @@ -7792,21 +7784,12 @@ .px-4 { padding-inline: calc(var(--spacing) * 4); } - .px-6 { - padding-inline: calc(var(--spacing) * 6); - } - .py-1 { - padding-block: calc(var(--spacing) * 1); - } .py-1\.5 { padding-block: calc(var(--spacing) * 1.5); } .py-2 { padding-block: calc(var(--spacing) * 2); } - .py-10 { - padding-block: calc(var(--spacing) * 10); - } .ps-2 { padding-inline-start: calc(var(--spacing) * 2); } @@ -8457,6 +8440,9 @@ .text-gray-400 { color: var(--color-gray-400); } + .text-green-400 { + color: var(--color-green-400); + } .text-info { color: var(--color-info); } @@ -9348,13 +9334,6 @@ border-color: var(--color-red-500); } } - .hover\:bg-gray-300 { - &:hover { - @media (hover: hover) { - background-color: var(--color-gray-300); - } - } - } .hover\:bg-red-600 { &:hover { @media (hover: hover) { @@ -9362,13 +9341,6 @@ } } } - .hover\:text-gray-800 { - &:hover { - @media (hover: hover) { - color: var(--color-gray-800); - } - } - } .focus\:outline { &:focus { outline-style: var(--tw-outline-style); @@ -9385,14 +9357,9 @@ outline-color: var(--color-indigo-600); } } - .data-\[active\=true\]\:bg-gray-300 { - &[data-active="true"] { - background-color: var(--color-gray-300); - } - } - .data-\[active\=true\]\:text-gray-800 { - &[data-active="true"] { - color: var(--color-gray-800); + .md\:hidden { + @media (width >= 48rem) { + display: none; } } .md\:grid-cols-2 { @@ -9400,6 +9367,64 @@ grid-template-columns: repeat(2, minmax(0, 1fr)); } } + .lg\:drawer-open { + @media (width >= 64rem) { + @layer daisyui.l1.l2.l3 { + > .drawer-toggle:checked { + ~ .drawer-side { + scrollbar-color: revert-layer; + } + :root:has(&) { + --page-overflow: revert-layer; + --page-scroll-gutter: revert-layer; + --page-scroll-bg: revert-layer; + --page-scroll-transition: revert-layer; + --page-has-backdrop: revert-layer; + animation: revert-layer; + animation-timeline: revert-layer; + } + } + } + @layer daisyui.l1.l2 { + > .drawer-side { + overflow-y: auto; + } + > .drawer-toggle { + display: none; + ~ .drawer-side { + pointer-events: auto; + visibility: visible; + position: sticky; + display: block; + width: auto; + overscroll-behavior: auto; + opacity: 100%; + > .drawer-overlay { + cursor: default; + background-color: transparent; + } + } + &:checked ~ .drawer-side { + pointer-events: auto; + visibility: visible; + } + } + } + @layer daisyui.l1 { + > .drawer-toggle ~ .drawer-side > :not(.drawer-overlay) { + translate: 0%; + [dir="rtl"] & { + translate: 0%; + } + } + } + } + } + .lg\:hidden { + @media (width >= 64rem) { + display: none; + } + } .lg\:grid-cols-3 { @media (width >= 64rem) { grid-template-columns: repeat(3, minmax(0, 1fr)); @@ -9410,6 +9435,149 @@ color: var(--color-white); } } + .is-drawer-close\:tooltip { + &:where(.drawer-toggle:not(:checked) ~ .drawer-side, .drawer-toggle:not(:checked) ~ .drawer-side *) { + @layer daisyui.l1.l2.l3 { + position: relative; + display: inline-block; + --tt-bg: var(--color-neutral); + --tt-off: calc(100% + 0.5rem); + --tt-tail: calc(100% + 1px + 0.25rem); + & > .tooltip-content, &[data-tip]:before { + position: absolute; + max-width: 20rem; + border-radius: var(--radius-field); + padding-inline: calc(0.25rem * 2); + padding-block: calc(0.25rem * 1); + text-align: center; + white-space: normal; + color: var(--color-neutral-content); + opacity: 0%; + font-size: 0.875rem; + line-height: 1.25; + background-color: var(--tt-bg); + width: max-content; + pointer-events: none; + z-index: 2; + --tw-content: attr(data-tip); + content: var(--tw-content); + } + &:after { + opacity: 0%; + background-color: var(--tt-bg); + content: ""; + pointer-events: none; + width: 0.625rem; + height: 0.25rem; + display: block; + position: absolute; + mask-repeat: no-repeat; + mask-position: -1px 0; + --mask-tooltip: url("data:image/svg+xml,%3Csvg width='10' height='4' viewBox='0 0 8 4' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0.500009 1C3.5 1 3.00001 4 5.00001 4C7 4 6.5 1 9.5 1C10 1 10 0.499897 10 0H0C-1.99338e-08 0.5 0 1 0.500009 1Z' fill='black'/%3E%3C/svg%3E%0A"); + mask-image: var(--mask-tooltip); + } + @media (prefers-reduced-motion: no-preference) { + & > .tooltip-content, &[data-tip]:before, &:after { + transition: opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1) 75ms, transform 0.2s cubic-bezier(0.4, 0, 0.2, 1) 75ms; + } + } + &:is([data-tip]:not([data-tip=""]), :has(.tooltip-content:not(:empty))) { + &.tooltip-open, &:hover, &:has(:focus-visible) { + & > .tooltip-content, &[data-tip]:before, &:after { + opacity: 100%; + --tt-pos: 0rem; + @media (prefers-reduced-motion: no-preference) { + transition: opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1) 0s, transform 0.2s cubic-bezier(0.4, 0, 0.2, 1) 0s; + } + } + } + } + } + @layer daisyui.l1.l2 { + > .tooltip-content, &[data-tip]:before { + transform: translateX(-50%) translateY(var(--tt-pos, 0.25rem)); + inset: auto auto var(--tt-off) 50%; + } + &:after { + transform: translateX(-50%) translateY(var(--tt-pos, 0.25rem)); + inset: auto auto var(--tt-tail) 50%; + } + } + } + } + .is-drawer-close\:tooltip-right { + &:where(.drawer-toggle:not(:checked) ~ .drawer-side, .drawer-toggle:not(:checked) ~ .drawer-side *) { + @layer daisyui.l1.l2 { + > .tooltip-content, &[data-tip]:before { + transform: translateX(calc(var(--tt-pos, -0.25rem) + 0.25rem)) translateY(-50%); + inset: 50% auto auto var(--tt-off); + } + &:after { + transform: translateX(var(--tt-pos, -0.25rem)) translateY(-50%) rotate(90deg); + inset: 50% auto auto calc(var(--tt-tail) + 1px); + } + } + } + } + .is-drawer-close\:hidden { + &:where(.drawer-toggle:not(:checked) ~ .drawer-side, .drawer-toggle:not(:checked) ~ .drawer-side *) { + display: none; + } + } + .is-drawer-close\:w-14 { + &:where(.drawer-toggle:not(:checked) ~ .drawer-side, .drawer-toggle:not(:checked) ~ .drawer-side *) { + width: calc(var(--spacing) * 14); + } + } + .is-drawer-close\:min-w-\[80px\] { + &:where(.drawer-toggle:not(:checked) ~ .drawer-side, .drawer-toggle:not(:checked) ~ .drawer-side *) { + min-width: 80px; + } + } + .is-drawer-close\:items-center { + &:where(.drawer-toggle:not(:checked) ~ .drawer-side, .drawer-toggle:not(:checked) ~ .drawer-side *) { + align-items: center; + } + } + .is-drawer-close\:overflow-visible { + &:where(.drawer-toggle:not(:checked) ~ .drawer-side, .drawer-toggle:not(:checked) ~ .drawer-side *) { + overflow: visible; + } + } + .is-drawer-close\:text-error { + &:where(.drawer-toggle:not(:checked) ~ .drawer-side, .drawer-toggle:not(:checked) ~ .drawer-side *) { + color: var(--color-error); + } + } + .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\:w-64 { + &:where(.drawer-toggle:checked ~ .drawer-side, .drawer-toggle:checked ~ .drawer-side *) { + width: calc(var(--spacing) * 64); + } + } + .is-drawer-open\:min-w-\[340px\] { + &:where(.drawer-toggle:checked ~ .drawer-side, .drawer-toggle:checked ~ .drawer-side *) { + min-width: 340px; + } + } + .is-drawer-open\:justify-between { + &:where(.drawer-toggle:checked ~ .drawer-side, .drawer-toggle:checked ~ .drawer-side *) { + justify-content: space-between; + } + } + .is-drawer-open\:space-x-4 { + &:where(.drawer-toggle:checked ~ .drawer-side, .drawer-toggle:checked ~ .drawer-side *) { + :where(& > :not(:last-child)) { + --tw-space-x-reverse: 0; + margin-inline-start: calc(calc(var(--spacing) * 4) * var(--tw-space-x-reverse)); + margin-inline-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse))); + } + } + } } @layer base { :where(:root),:root:has(input.theme-controller[value=light]:checked),[data-theme=light] { diff --git a/Sources/ApiController/Live.swift b/Sources/ApiController/Live.swift index 67b2d33..097586b 100644 --- a/Sources/ApiController/Live.swift +++ b/Sources/ApiController/Live.swift @@ -31,6 +31,13 @@ extension SiteRoute.Api.ProjectRoute { case .delete(let id): try await database.projects.delete(id) return nil + case .detail(let id, let route): + switch route { + case .completedSteps: + // FIX: + fatalError() + + } case .get(let id): guard let project = try await database.projects.get(id) else { logger.error("Project not found for id: \(id)") diff --git a/Sources/App/Middleware/ViewRoute+middleware.swift b/Sources/App/Middleware/ViewRoute+middleware.swift index 0c3023f..0155e59 100644 --- a/Sources/App/Middleware/ViewRoute+middleware.swift +++ b/Sources/App/Middleware/ViewRoute+middleware.swift @@ -16,7 +16,7 @@ extension SiteRoute.View { switch self { case .project: return viewRouteMiddleware - case .login, .signup: + case .login, .signup, .test: return nil } } diff --git a/Sources/DatabaseClient/Projects.swift b/Sources/DatabaseClient/Projects.swift index b685fde..90ab1f8 100644 --- a/Sources/DatabaseClient/Projects.swift +++ b/Sources/DatabaseClient/Projects.swift @@ -10,6 +10,7 @@ extension DatabaseClient { public var create: @Sendable (User.ID, Project.Create) async throws -> Project public var delete: @Sendable (Project.ID) async throws -> Void public var get: @Sendable (Project.ID) async throws -> Project? + public var getCompletedSteps: @Sendable (Project.ID) async throws -> Project.CompletedSteps public var getSensibleHeatRatio: @Sendable (Project.ID) async throws -> Double? public var fetch: @Sendable (User.ID, PageRequest) async throws -> Page public var update: @Sendable (Project.Update) async throws -> Project @@ -35,6 +36,41 @@ extension DatabaseClient.Projects: TestDependencyKey { get: { id in try await ProjectModel.find(id, on: database).map { try $0.toDTO() } }, + getCompletedSteps: { id in + let roomsCount = try await RoomModel.query(on: database) + .with(\.$project) + .filter(\.$project.$id == id) + .count() + + let equivalentLengths = try await EffectiveLengthModel.query(on: database) + .with(\.$project) + .filter(\.$project.$id == id) + .all() + + var equivalentLengthsCompleted = false + + if equivalentLengths.filter({ $0.type == "supply" }).first != nil, + equivalentLengths.filter({ $0.type == "return" }).first != nil + { + equivalentLengthsCompleted = true + } + + let componentLosses = try await ComponentLossModel.query(on: database) + .with(\.$project) + .filter(\.$project.$id == id) + .count() + + let equipmentInfo = try await EquipmentModel.query(on: database) + .with(\.$project) + .filter(\.$project.$id == id) + .first() + + return .init( + rooms: roomsCount > 0, + equivalentLength: equivalentLengthsCompleted, + frictionRate: equipmentInfo != nil && componentLosses > 0 + ) + }, getSensibleHeatRatio: { id in guard let model = try await ProjectModel.find(id, on: database) else { throw NotFoundError() diff --git a/Sources/ManualDCore/Project.swift b/Sources/ManualDCore/Project.swift index 06ec67e..2c3c6ca 100644 --- a/Sources/ManualDCore/Project.swift +++ b/Sources/ManualDCore/Project.swift @@ -64,6 +64,19 @@ extension Project { } } + public struct CompletedSteps: Codable, Equatable, Sendable { + + public let rooms: Bool + public let equivalentLength: Bool + public let frictionRate: Bool + + public init(rooms: Bool, equivalentLength: Bool, frictionRate: Bool) { + self.rooms = rooms + self.equivalentLength = equivalentLength + self.frictionRate = frictionRate + } + } + public struct Update: Codable, Equatable, Sendable { public let id: Project.ID diff --git a/Sources/ManualDCore/Routes/ApiRoute.swift b/Sources/ManualDCore/Routes/ApiRoute.swift index 35f53b2..97ae36f 100644 --- a/Sources/ManualDCore/Routes/ApiRoute.swift +++ b/Sources/ManualDCore/Routes/ApiRoute.swift @@ -45,6 +45,7 @@ extension SiteRoute.Api { public enum ProjectRoute: Sendable, Equatable { case create(Project.Create) case delete(id: Project.ID) + case detail(id: Project.ID, route: DetailRoute) case get(id: Project.ID) case index @@ -74,6 +75,31 @@ extension SiteRoute.Api { Path { rootPath } Method.get } + Route(.case(Self.detail(id:route:))) { + Path { + rootPath + Project.ID.parser() + } + DetailRoute.router + } + } + } +} + +extension SiteRoute.Api.ProjectRoute { + public enum DetailRoute: Equatable, Sendable { + case completedSteps + + static let rootPath = "details" + + static let router = OneOf { + Route(.case(Self.completedSteps)) { + Path { + rootPath + "completed" + } + Method.get + } } } } diff --git a/Sources/ManualDCore/Routes/ViewRoute.swift b/Sources/ManualDCore/Routes/ViewRoute.swift index 9d32908..5eced86 100644 --- a/Sources/ManualDCore/Routes/ViewRoute.swift +++ b/Sources/ManualDCore/Routes/ViewRoute.swift @@ -11,8 +11,14 @@ extension SiteRoute { case login(LoginRoute) case signup(SignupRoute) case project(ProjectRoute) + //FIX: Remove. + case test public static let router = OneOf { + Route(.case(Self.test)) { + Path { "test" } + Method.get + } Route(.case(Self.login)) { SiteRoute.View.LoginRoute.router } diff --git a/Sources/Styleguide/Icon.swift b/Sources/Styleguide/Icon.swift index f689fa1..2d816c7 100644 --- a/Sources/Styleguide/Icon.swift +++ b/Sources/Styleguide/Icon.swift @@ -1,5 +1,6 @@ import Elementary +// TODO: Remove, using svg's. public struct Icon: HTML, Sendable { let icon: String diff --git a/Sources/Styleguide/SVG.swift b/Sources/Styleguide/SVG.swift index 6da5d16..a6441a5 100644 --- a/Sources/Styleguide/SVG.swift +++ b/Sources/Styleguide/SVG.swift @@ -15,17 +15,33 @@ public struct SVG: HTML, Sendable { extension SVG { public enum Key: Sendable { + case badgeCheck + case ban case chevronRight case circlePlus case close + case doorClosed case email case key + case mapPin + case rulerDimensionLine + case sidebarToggle + case squareFunction case squarePen case trash case user + case wind var svg: String { switch self { + case .badgeCheck: + return """ + + """ + case .ban: + return """ + + """ case .chevronRight: return """ @@ -38,6 +54,10 @@ extension SVG { return """ """ + case .doorClosed: + return """ + + """ case .email: return """ @@ -70,6 +90,22 @@ extension SVG { """ + case .mapPin: + return """ + + """ + case .rulerDimensionLine: + return """ + + """ + case .sidebarToggle: + return """ + + """ + case .squareFunction: + return """ + + """ case .squarePen: return """ @@ -94,6 +130,10 @@ extension SVG { """ + case .wind: + return """ + + """ } } } diff --git a/Sources/ViewController/Live.swift b/Sources/ViewController/Live.swift index 0d7a583..df5fb8e 100644 --- a/Sources/ViewController/Live.swift +++ b/Sources/ViewController/Live.swift @@ -11,6 +11,10 @@ extension ViewController.Request { @Dependency(\.database) var database switch route { + case .test: + return view { + TestPage() + } case .login(let route): switch route { case .index(let next): diff --git a/Sources/ViewController/Views/Project/ProjectView.swift b/Sources/ViewController/Views/Project/ProjectView.swift index 0db196a..e9b88f9 100644 --- a/Sources/ViewController/Views/Project/ProjectView.swift +++ b/Sources/ViewController/Views/Project/ProjectView.swift @@ -22,10 +22,18 @@ struct ProjectView: HTML, Sendable { } var body: some HTML { - div { - div(.class("flex flex-row")) { - Sidebar(active: activeTab, projectID: projectID) - main(.class("flex flex-col h-screen w-full px-6 py-10")) { + div(.class("h-screen w-full")) { + + div(.class("drawer lg:drawer-open")) { + input(.id("my-drawer-1"), .type(.checkbox), .class("drawer-toggle")) + + div(.class("drawer-content p-4")) { + label( + .for("my-drawer-1"), + .class("btn btn-square btn-ghost drawer-button size-7") + ) { + SVG(.sidebarToggle) + } switch self.activeTab { case .project: if let project = try await database.projects.get(projectID) { @@ -56,6 +64,12 @@ struct ProjectView: HTML, Sendable { } } + + try await Sidebar( + active: activeTab, + projectID: projectID, + completedSteps: database.projects.getCompletedSteps(projectID) + ) } } } @@ -66,99 +80,164 @@ struct Sidebar: HTML { let active: SiteRoute.View.ProjectRoute.DetailRoute.Tab let projectID: Project.ID + let completedSteps: Project.CompletedSteps var body: some HTML { - aside( - .class( - """ - h-screen sticky top-0 max-w-[280px] flex-none - border-r-2 border-gray-200 - shadow-lg - """ - ) - ) { - div(.class("flex")) { - // TODO: Move somewhere outside of the sidebar. - button( - .class("btn btn-secondary btn-block"), - .hx.get(route: .project(.index)), - .hx.target("body"), - .hx.pushURL(true), - .hx.swap(.outerHTML), - ) { - "< All Projects" + div(.class("drawer-side is-drawer-close:overflow-visible")) { + label( + .for("my-drawer-1"), .init(name: "aria-label", value: "close sidebar"), + .class("drawer-overlay") + ) {} + + div( + .class( + """ + flex min-h-full flex-col items-start bg-base-200 + is-drawer-close:min-w-[80px] is-drawer-open:min-w-[340px] + """ + ) + ) { + + ul(.class("w-full")) { + + li(.class("w-full")) { + div( + .class("w-full is-drawer-close:tooltip is-drawer-close:tooltip-right"), + .data("tip", value: "All Projects") + ) { + a( + .class( + """ + flex btn btn-secondary btn-square btn-block + is-drawer-close:items-center + """ + ), + .hx.get(route: .project(.index)), + .hx.target("body"), + .hx.pushURL(true), + .hx.swap(.outerHTML), + ) { + div(.class("flex is-drawer-open:space-x-4")) { + span { "<" } + span(.class("is-drawer-close:hidden")) { "All Projects" } + } + } + } + } + + // FIX: Move to user profile / settings page. + li(.class("w-full is-drawer-close:hidden")) { + div(.class("flex justify-between p-4")) { + Label("Theme") + input(.type(.checkbox), .class("toggle theme-controller"), .value("light")) + } + } + + li(.class("w-full")) { + row( + title: "Project", + icon: .mapPin, + route: .project(.detail(projectID, .index(tab: .project))), + isComplete: true + ) + .attributes(.class("btn-active"), when: active == .project) + } + + li(.class("w-full")) { + row( + title: "Rooms", + icon: .doorClosed, + route: .project(.detail(projectID, .rooms(.index))), + isComplete: completedSteps.rooms + ) + .attributes(.class("btn-active"), when: active == .rooms) + } + + li(.class("w-full")) { + row( + title: "Equivalent Lengths", + icon: .rulerDimensionLine, + route: .project(.detail(projectID, .equivalentLength(.index))), + isComplete: completedSteps.equivalentLength + ) + .attributes(.class("btn-active"), when: active == .equivalentLength) + + } + li(.class("w-full")) { + row( + title: "Friction Rate", + icon: .squareFunction, + route: .project(.detail(projectID, .frictionRate(.index))), + isComplete: completedSteps.frictionRate + ) + .attributes(.class("btn-active"), when: active == .frictionRate) + + } + li(.class("w-full")) { + row( + title: "Duct Sizes", icon: .wind, href: "#", isComplete: false, hideIsComplete: true + ) + .attributes(.class("btn-active"), when: active == .ductSizing) + } } } - - Row { - Label("Theme") - input(.type(.checkbox), .class("toggle theme-controller"), .value("light")) - } - .attributes(.class("p-4")) - - row( - title: "Project", - icon: .mapPin, - route: .project(.detail(projectID, .index(tab: .project))) - ) - .attributes(.data("active", value: active == .project ? "true" : "false")) - - row( - title: "Rooms", - icon: .doorClosed, - route: .project(.detail(projectID, .rooms(.index))) - ) - .attributes(.data("active", value: active == .rooms ? "true" : "false")) - - row( - title: "Equivalent Lengths", - icon: .rulerDimensionLine, - route: .project(.detail(projectID, .equivalentLength(.index))) - ) - .attributes(.data("active", value: active == .equivalentLength ? "true" : "false")) - - row( - title: "Friction Rate", - icon: .squareFunction, - route: .project(.detail(projectID, .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 { - a( + icon: SVG.Key, + href: String, + isComplete: Bool, + hideIsComplete: Bool = false + ) -> some HTML { + div( .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 - """ + "w-full is-drawer-close:tooltip is-drawer-close:tooltip-right" ), - .href(href) + .data("tip", value: title) ) { - Icon(icon) - span(.class("text-xl")) { - title + a( + .class( + "flex btn btn-soft btn-square btn-block is-drawer-open:justify-between is-drawer-close:items-center" + ), + .href(href) + ) { + div(.class("flex is-drawer-open:space-x-4")) { + SVG(icon) + span(.class("text-xl is-drawer-close:hidden")) { + title + } + } + if !hideIsComplete { + div(.class("is-drawer-close:hidden")) { + if isComplete { + SVG(.badgeCheck) + } else { + SVG(.ban) + } + } + .attributes(.class("text-green-400"), when: isComplete) + .attributes(.class("text-error"), when: !isComplete) + } } + .attributes(.class("is-drawer-close:text-green-400"), when: isComplete) + .attributes(.class("is-drawer-close:text-error"), when: !isComplete && !hideIsComplete) } } private func row( title: String, - icon: Icon.Key, - route: SiteRoute.View - ) -> some HTML { - row(title: title, icon: icon, href: SiteRoute.View.router.path(for: route)) + icon: SVG.Key, + route: SiteRoute.View, + isComplete: Bool, + hideIsComplete: Bool = false + ) -> some HTML { + row( + title: title, icon: icon, href: SiteRoute.View.router.path(for: route), + isComplete: isComplete, hideIsComplete: hideIsComplete + ) } } diff --git a/Sources/ViewController/Views/TestPage.swift b/Sources/ViewController/Views/TestPage.swift new file mode 100644 index 0000000..d37b522 --- /dev/null +++ b/Sources/ViewController/Views/TestPage.swift @@ -0,0 +1,51 @@ +import Elementary + +struct TestPage: HTML, Sendable { + var body: some HTML { + HTMLRaw( + """ +
+ +
+ + + +
Page Content
+
+ +
+ +
+ + +
+
+
+ """ + ) + } +}