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;
--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] {

View File

@@ -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)")

View File

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

View File

@@ -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<Project>
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()

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 let id: Project.ID

View File

@@ -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
}
}
}
}

View File

@@ -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
}

View File

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

View File

@@ -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 """
<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:
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>
@@ -38,6 +54,10 @@ extension SVG {
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>
"""
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:
return """
<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>
</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:
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>
@@ -94,6 +130,10 @@ extension SVG {
</g>
</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
switch route {
case .test:
return view {
TestPage()
}
case .login(let route):
switch route {
case .index(let next):

View File

@@ -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<HTMLTag.a> {
a(
icon: SVG.Key,
href: String,
isComplete: Bool,
hideIsComplete: Bool = false
) -> some HTML<HTMLTag.div> {
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<HTMLTag.a> {
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<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>
"""
)
}
}