feat: Updates sidebar to use the drawer classes from daisyui, currently doesn't open automatically on large screens like I want.

This commit is contained in:
2026-01-08 12:40:05 -05:00
parent 79b7892d9a
commit 9356ccb1c9
12 changed files with 578 additions and 147 deletions

View File

@@ -9,18 +9,15 @@
monospace; monospace;
--color-red-500: oklch(63.7% 0.237 25.331); --color-red-500: oklch(63.7% 0.237 25.331);
--color-red-600: oklch(57.7% 0.245 27.325); --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-indigo-600: oklch(51.1% 0.262 276.966);
--color-slate-300: oklch(86.9% 0.022 252.894); --color-slate-300: oklch(86.9% 0.022 252.894);
--color-slate-900: oklch(20.8% 0.042 265.755); --color-slate-900: oklch(20.8% 0.042 265.755);
--color-gray-200: oklch(92.8% 0.006 264.531); --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-400: oklch(70.7% 0.022 261.325);
--color-gray-800: oklch(27.8% 0.033 256.848);
--color-black: #000; --color-black: #000;
--color-white: #fff; --color-white: #fff;
--spacing: 0.25rem; --spacing: 0.25rem;
--text-xs: 0.75rem;
--text-xs--line-height: calc(1 / 0.75);
--text-sm: 0.875rem; --text-sm: 0.875rem;
--text-sm--line-height: calc(1.25 / 0.875); --text-sm--line-height: calc(1.25 / 0.875);
--text-lg: 1.125rem; --text-lg: 1.125rem;
@@ -4225,21 +4222,12 @@
--toast-y: 0; --toast-y: 0;
} }
} }
.top-0 {
top: calc(var(--spacing) * 0);
}
.top-2 { .top-2 {
top: calc(var(--spacing) * 2); top: calc(var(--spacing) * 2);
} }
.right-2 { .right-2 {
right: calc(var(--spacing) * 2); right: calc(var(--spacing) * 2);
} }
.right-4 {
right: calc(var(--spacing) * 4);
}
.right-6 {
right: calc(var(--spacing) * 6);
}
.dock-sm { .dock-sm {
@layer daisyui.l1.l2 { @layer daisyui.l1.l2 {
height: calc(0.25rem * 14); height: calc(0.25rem * 14);
@@ -4297,12 +4285,6 @@
} }
} }
} }
.bottom-4 {
bottom: calc(var(--spacing) * 4);
}
.bottom-6 {
bottom: calc(var(--spacing) * 6);
}
.join { .join {
display: inline-flex; display: inline-flex;
align-items: stretch; align-items: stretch;
@@ -5289,6 +5271,9 @@
.mx-2 { .mx-2 {
margin-inline: calc(var(--spacing) * 2); margin-inline: calc(var(--spacing) * 2);
} }
.mx-4 {
margin-inline: calc(var(--spacing) * 4);
}
.file-input-ghost { .file-input-ghost {
@layer daisyui.l1.l2 { @layer daisyui.l1.l2 {
background-color: transparent; background-color: transparent;
@@ -5375,6 +5360,9 @@
} }
} }
} }
.my-1\.5 {
margin-block: calc(var(--spacing) * 1.5);
}
.my-2 { .my-2 {
margin-block: calc(var(--spacing) * 2); margin-block: calc(var(--spacing) * 2);
} }
@@ -6380,6 +6368,14 @@
height: var(--size); 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 { .status-lg {
@layer daisyui.l1.l2 { @layer daisyui.l1.l2 {
width: calc(0.25rem * 3); width: calc(0.25rem * 3);
@@ -6428,6 +6424,9 @@
.h-screen { .h-screen {
height: 100vh; height: 100vh;
} }
.min-h-full {
min-height: 100%;
}
.btn-wide { .btn-wide {
@layer daisyui.l1.l2 { @layer daisyui.l1.l2 {
width: 100%; width: 100%;
@@ -6559,18 +6558,15 @@
.w-full { .w-full {
width: 100%; width: 100%;
} }
.max-w-\[280px\] {
max-width: 280px;
}
.flex-none {
flex: none;
}
.flex-shrink { .flex-shrink {
flex-shrink: 1; flex-shrink: 1;
} }
.flex-grow { .flex-grow {
flex-grow: 1; flex-grow: 1;
} }
.grow {
flex-grow: 1;
}
.border-collapse { .border-collapse {
border-collapse: collapse; border-collapse: collapse;
} }
@@ -6755,14 +6751,11 @@
.flex-col { .flex-col {
flex-direction: column; flex-direction: column;
} }
.flex-row {
flex-direction: row;
}
.flex-wrap { .flex-wrap {
flex-wrap: wrap; flex-wrap: wrap;
} }
.items-center { .items-start {
align-items: center; align-items: flex-start;
} }
.justify-between { .justify-between {
justify-content: space-between; justify-content: space-between;
@@ -7095,10 +7088,6 @@
border-style: var(--tw-border-style); border-style: var(--tw-border-style);
border-width: 1px; border-width: 1px;
} }
.border-r-2 {
border-right-style: var(--tw-border-style);
border-right-width: 2px;
}
.border-b { .border-b {
border-bottom-style: var(--tw-border-style); border-bottom-style: var(--tw-border-style);
border-bottom-width: 1px; border-bottom-width: 1px;
@@ -7375,6 +7364,12 @@
.bg-base-100 { .bg-base-100 {
background-color: var(--color-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 { .bg-red-500 {
background-color: var(--color-red-500); background-color: var(--color-red-500);
} }
@@ -7666,9 +7661,6 @@
.p-4 { .p-4 {
padding: calc(var(--spacing) * 4); padding: calc(var(--spacing) * 4);
} }
.p-6 {
padding: calc(var(--spacing) * 6);
}
.menu-title { .menu-title {
@layer daisyui.l1.l2.l3 { @layer daisyui.l1.l2.l3 {
padding-inline: calc(0.25rem * 3); padding-inline: calc(0.25rem * 3);
@@ -7792,21 +7784,12 @@
.px-4 { .px-4 {
padding-inline: calc(var(--spacing) * 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 { .py-1\.5 {
padding-block: calc(var(--spacing) * 1.5); padding-block: calc(var(--spacing) * 1.5);
} }
.py-2 { .py-2 {
padding-block: calc(var(--spacing) * 2); padding-block: calc(var(--spacing) * 2);
} }
.py-10 {
padding-block: calc(var(--spacing) * 10);
}
.ps-2 { .ps-2 {
padding-inline-start: calc(var(--spacing) * 2); padding-inline-start: calc(var(--spacing) * 2);
} }
@@ -8457,6 +8440,9 @@
.text-gray-400 { .text-gray-400 {
color: var(--color-gray-400); color: var(--color-gray-400);
} }
.text-green-400 {
color: var(--color-green-400);
}
.text-info { .text-info {
color: var(--color-info); color: var(--color-info);
} }
@@ -9348,13 +9334,6 @@
border-color: var(--color-red-500); 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\:bg-red-600 {
&:hover { &:hover {
@media (hover: 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 {
&:focus { &:focus {
outline-style: var(--tw-outline-style); outline-style: var(--tw-outline-style);
@@ -9385,14 +9357,9 @@
outline-color: var(--color-indigo-600); outline-color: var(--color-indigo-600);
} }
} }
.data-\[active\=true\]\:bg-gray-300 { .md\:hidden {
&[data-active="true"] { @media (width >= 48rem) {
background-color: var(--color-gray-300); display: none;
}
}
.data-\[active\=true\]\:text-gray-800 {
&[data-active="true"] {
color: var(--color-gray-800);
} }
} }
.md\:grid-cols-2 { .md\:grid-cols-2 {
@@ -9400,6 +9367,64 @@
grid-template-columns: repeat(2, minmax(0, 1fr)); 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 { .lg\:grid-cols-3 {
@media (width >= 64rem) { @media (width >= 64rem) {
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
@@ -9410,6 +9435,149 @@
color: var(--color-white); 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 { @layer base {
:where(:root),:root:has(input.theme-controller[value=light]:checked),[data-theme=light] { :where(:root),:root:has(input.theme-controller[value=light]:checked),[data-theme=light] {

View File

@@ -31,6 +31,13 @@ extension SiteRoute.Api.ProjectRoute {
case .delete(let id): case .delete(let id):
try await database.projects.delete(id) try await database.projects.delete(id)
return nil return nil
case .detail(let id, let route):
switch route {
case .completedSteps:
// FIX:
fatalError()
}
case .get(let id): case .get(let id):
guard let project = try await database.projects.get(id) else { guard let project = try await database.projects.get(id) else {
logger.error("Project not found for id: \(id)") logger.error("Project not found for id: \(id)")

View File

@@ -16,7 +16,7 @@ extension SiteRoute.View {
switch self { switch self {
case .project: case .project:
return viewRouteMiddleware return viewRouteMiddleware
case .login, .signup: case .login, .signup, .test:
return nil return nil
} }
} }

View File

@@ -10,6 +10,7 @@ extension DatabaseClient {
public var create: @Sendable (User.ID, Project.Create) async throws -> Project public var create: @Sendable (User.ID, Project.Create) async throws -> Project
public var delete: @Sendable (Project.ID) async throws -> Void public var delete: @Sendable (Project.ID) async throws -> Void
public var get: @Sendable (Project.ID) async throws -> Project? 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 getSensibleHeatRatio: @Sendable (Project.ID) async throws -> Double?
public var fetch: @Sendable (User.ID, PageRequest) async throws -> Page<Project> public var fetch: @Sendable (User.ID, PageRequest) async throws -> Page<Project>
public var update: @Sendable (Project.Update) async throws -> Project public var update: @Sendable (Project.Update) async throws -> Project
@@ -35,6 +36,41 @@ extension DatabaseClient.Projects: TestDependencyKey {
get: { id in get: { id in
try await ProjectModel.find(id, on: database).map { try $0.toDTO() } 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 getSensibleHeatRatio: { id in
guard let model = try await ProjectModel.find(id, on: database) else { guard let model = try await ProjectModel.find(id, on: database) else {
throw NotFoundError() throw NotFoundError()

View File

@@ -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 struct Update: Codable, Equatable, Sendable {
public let id: Project.ID public let id: Project.ID

View File

@@ -45,6 +45,7 @@ extension SiteRoute.Api {
public enum ProjectRoute: Sendable, Equatable { public enum ProjectRoute: Sendable, Equatable {
case create(Project.Create) case create(Project.Create)
case delete(id: Project.ID) case delete(id: Project.ID)
case detail(id: Project.ID, route: DetailRoute)
case get(id: Project.ID) case get(id: Project.ID)
case index case index
@@ -74,6 +75,31 @@ extension SiteRoute.Api {
Path { rootPath } Path { rootPath }
Method.get 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
}
} }
} }
} }

View File

@@ -11,8 +11,14 @@ extension SiteRoute {
case login(LoginRoute) case login(LoginRoute)
case signup(SignupRoute) case signup(SignupRoute)
case project(ProjectRoute) case project(ProjectRoute)
//FIX: Remove.
case test
public static let router = OneOf { public static let router = OneOf {
Route(.case(Self.test)) {
Path { "test" }
Method.get
}
Route(.case(Self.login)) { Route(.case(Self.login)) {
SiteRoute.View.LoginRoute.router SiteRoute.View.LoginRoute.router
} }

View File

@@ -1,5 +1,6 @@
import Elementary import Elementary
// TODO: Remove, using svg's.
public struct Icon: HTML, Sendable { public struct Icon: HTML, Sendable {
let icon: String let icon: String

View File

@@ -15,17 +15,33 @@ public struct SVG: HTML, Sendable {
extension SVG { extension SVG {
public enum Key: Sendable { public enum Key: Sendable {
case badgeCheck
case ban
case chevronRight case chevronRight
case circlePlus case circlePlus
case close case close
case doorClosed
case email case email
case key case key
case mapPin
case rulerDimensionLine
case sidebarToggle
case squareFunction
case squarePen case squarePen
case trash case trash
case user case user
case wind
var svg: String { var svg: String {
switch self { switch self {
case .badgeCheck:
return """
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-check-icon lucide-badge-check"><path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"/><path d="m9 12 2 2 4-4"/></svg>
"""
case .ban:
return """
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-ban-icon lucide-ban"><path d="M4.929 4.929 19.07 19.071"/><circle cx="12" cy="12" r="10"/></svg>
"""
case .chevronRight: case .chevronRight:
return """ return """
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-right-icon lucide-chevron-right"><path d="m9 18 6-6-6-6"/></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-right-icon lucide-chevron-right"><path d="m9 18 6-6-6-6"/></svg>
@@ -38,6 +54,10 @@ extension SVG {
return """ return """
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
""" """
case .doorClosed:
return """
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-door-closed-icon lucide-door-closed"><path d="M10 12h.01"/><path d="M18 20V6a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v14"/><path d="M2 20h20"/></svg>
"""
case .email: case .email:
return """ return """
<svg class="h-[1em] opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <svg class="h-[1em] opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
@@ -70,6 +90,22 @@ extension SVG {
</g> </g>
</svg> </svg>
""" """
case .mapPin:
return """
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-map-pin-icon lucide-map-pin"><path d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"/><circle cx="12" cy="10" r="3"/></svg>
"""
case .rulerDimensionLine:
return """
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-ruler-dimension-line-icon lucide-ruler-dimension-line"><path d="M10 15v-3"/><path d="M14 15v-3"/><path d="M18 15v-3"/><path d="M2 8V4"/><path d="M22 6H2"/><path d="M22 8V4"/><path d="M6 15v-3"/><rect x="2" y="12" width="20" height="8" rx="2"/></svg>
"""
case .sidebarToggle:
return """
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-linejoin="round" stroke-linecap="round" stroke-width="2" fill="none" stroke="currentColor" class="my-1.5 inline-block"><path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path><path d="M9 4v16"></path><path d="M14 10l2 2l-2 2"></path></svg>
"""
case .squareFunction:
return """
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-function-icon lucide-square-function"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><path d="M9 17c2 0 2.8-1 2.8-2.8V10c0-2 1-3.3 3.2-3"/><path d="M9 11.2h5.7"/></svg>
"""
case .squarePen: case .squarePen:
return """ return """
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-pen-icon lucide-square-pen"><path d="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-pen-icon lucide-square-pen"><path d="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z"/></svg>
@@ -94,6 +130,10 @@ extension SVG {
</g> </g>
</svg> </svg>
""" """
case .wind:
return """
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-wind-icon lucide-wind"><path d="M12.8 19.6A2 2 0 1 0 14 16H2"/><path d="M17.5 8a2.5 2.5 0 1 1 2 4H2"/><path d="M9.8 4.4A2 2 0 1 1 11 8H2"/></svg>
"""
} }
} }
} }

View File

@@ -11,6 +11,10 @@ extension ViewController.Request {
@Dependency(\.database) var database @Dependency(\.database) var database
switch route { switch route {
case .test:
return view {
TestPage()
}
case .login(let route): case .login(let route):
switch route { switch route {
case .index(let next): case .index(let next):

View File

@@ -22,10 +22,18 @@ struct ProjectView: HTML, Sendable {
} }
var body: some HTML { var body: some HTML {
div { div(.class("h-screen w-full")) {
div(.class("flex flex-row")) {
Sidebar(active: activeTab, projectID: projectID) div(.class("drawer lg:drawer-open")) {
main(.class("flex flex-col h-screen w-full px-6 py-10")) { 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 { switch self.activeTab {
case .project: case .project:
if let project = try await database.projects.get(projectID) { 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 active: SiteRoute.View.ProjectRoute.DetailRoute.Tab
let projectID: Project.ID let projectID: Project.ID
let completedSteps: Project.CompletedSteps
var body: some HTML { 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")) { div(.class("drawer-side is-drawer-close:overflow-visible")) {
// TODO: Move somewhere outside of the sidebar. label(
button( .for("my-drawer-1"), .init(name: "aria-label", value: "close sidebar"),
.class("btn btn-secondary btn-block"), .class("drawer-overlay")
.hx.get(route: .project(.index)), ) {}
.hx.target("body"),
.hx.pushURL(true), div(
.hx.swap(.outerHTML), .class(
) { """
"< All Projects" 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. // TODO: Use SiteRoute.View routes as href.
private func row( private func row(
title: String, title: String,
icon: Icon.Key, icon: SVG.Key,
href: String href: String,
) -> some HTML<HTMLTag.a> { isComplete: Bool,
a( hideIsComplete: Bool = false
) -> some HTML<HTMLTag.div> {
div(
.class( .class(
""" "w-full is-drawer-close:tooltip is-drawer-close:tooltip-right"
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) .data("tip", value: title)
) { ) {
Icon(icon) a(
span(.class("text-xl")) { .class(
title "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( private func row(
title: String, title: String,
icon: Icon.Key, icon: SVG.Key,
route: SiteRoute.View route: SiteRoute.View,
) -> some HTML<HTMLTag.a> { isComplete: Bool,
row(title: title, icon: icon, href: SiteRoute.View.router.path(for: route)) hideIsComplete: Bool = false
) -> some HTML<HTMLTag.div> {
row(
title: title, icon: icon, href: SiteRoute.View.router.path(for: route),
isComplete: isComplete, hideIsComplete: hideIsComplete
)
} }
} }

View File

@@ -0,0 +1,51 @@
import Elementary
struct TestPage: HTML, Sendable {
var body: some HTML {
HTMLRaw(
"""
<div class="drawer lg:drawer-open">
<input id="my-drawer-4" type="checkbox" class="drawer-toggle" checked/>
<div class="drawer-content">
<!-- Navbar -->
<nav class="navbar w-full bg-base-300">
<label for="my-drawer-4" aria-label="open sidebar" class="btn btn-square btn-ghost">
<!-- Sidebar toggle icon -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-linejoin="round" stroke-linecap="round" stroke-width="2" fill="none" stroke="currentColor" class="my-1.5 inline-block size-4"><path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path><path d="M9 4v16"></path><path d="M14 10l2 2l-2 2"></path></svg>
</label>
<div class="px-4">Navbar Title</div>
</nav>
<!-- Page content here -->
<div class="p-4">Page Content</div>
</div>
<div class="drawer-side is-drawer-close:overflow-visible">
<label for="my-drawer-4" aria-label="close sidebar" class="drawer-overlay"></label>
<div class="flex min-h-full flex-col items-start bg-base-200 is-drawer-close:w-14 is-drawer-open:w-64">
<!-- Sidebar content here -->
<ul class="menu w-full grow">
<!-- List item -->
<li>
<button class="is-drawer-close:tooltip is-drawer-close:tooltip-right" data-tip="Homepage">
<!-- Home icon -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-linejoin="round" stroke-linecap="round" stroke-width="2" fill="none" stroke="currentColor" class="my-1.5 inline-block size-4"><path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8"></path><path d="M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path></svg>
<span class="is-drawer-close:hidden">Homepage</span>
</button>
</li>
<!-- List item -->
<li>
<button class="is-drawer-close:tooltip is-drawer-close:tooltip-right" data-tip="Settings">
<!-- Settings icon -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-linejoin="round" stroke-linecap="round" stroke-width="2" fill="none" stroke="currentColor" class="my-1.5 inline-block size-4"><path d="M20 7h-9"></path><path d="M14 17H5"></path><circle cx="17" cy="17" r="3"></circle><circle cx="7" cy="7" r="3"></circle></svg>
<span class="is-drawer-close:hidden">Settings</span>
</button>
</li>
</ul>
</div>
</div>
</div>
"""
)
}
}