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:
@@ -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] {
|
||||
|
||||
@@ -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)")
|
||||
|
||||
@@ -16,7 +16,7 @@ extension SiteRoute.View {
|
||||
switch self {
|
||||
case .project:
|
||||
return viewRouteMiddleware
|
||||
case .login, .signup:
|
||||
case .login, .signup, .test:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Elementary
|
||||
|
||||
// TODO: Remove, using svg's.
|
||||
public struct Icon: HTML, Sendable {
|
||||
|
||||
let icon: String
|
||||
|
||||
@@ -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>
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
51
Sources/ViewController/Views/TestPage.swift
Normal file
51
Sources/ViewController/Views/TestPage.swift
Normal 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>
|
||||
"""
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user