WIP: Moves some common views to a Styleguide module, working on room table and form.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,3 +6,4 @@ DerivedData/
|
||||
.swiftpm/configuration/registries.json
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||
.netrc
|
||||
.swift-version
|
||||
|
||||
@@ -10,6 +10,7 @@ let package = Package(
|
||||
.library(name: "DatabaseClient", targets: ["DatabaseClient"]),
|
||||
.library(name: "ManualDCore", targets: ["ManualDCore"]),
|
||||
.library(name: "ManualDClient", targets: ["ManualDClient"]),
|
||||
.library(name: "Styleguide", targets: ["Styleguide"]),
|
||||
.library(name: "ViewController", targets: ["ViewController"]),
|
||||
],
|
||||
dependencies: [
|
||||
@@ -84,6 +85,14 @@ let package = Package(
|
||||
.product(name: "DependenciesMacros", package: "swift-dependencies"),
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "Styleguide",
|
||||
dependencies: [
|
||||
"ManualDCore",
|
||||
.product(name: "Elementary", package: "elementary"),
|
||||
.product(name: "ElementaryHTMX", package: "elementary-htmx"),
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
name: "ManualDClientTests",
|
||||
dependencies: [
|
||||
@@ -95,6 +104,7 @@ let package = Package(
|
||||
name: "ViewController",
|
||||
dependencies: [
|
||||
.target(name: "ManualDCore"),
|
||||
.target(name: "Styleguide"),
|
||||
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||
.product(name: "DependenciesMacros", package: "swift-dependencies"),
|
||||
.product(name: "Elementary", package: "elementary"),
|
||||
|
||||
@@ -62,11 +62,19 @@ extension SiteRoute.View {
|
||||
extension SiteRoute.View {
|
||||
public enum RoomRoute: Equatable, Sendable {
|
||||
case form
|
||||
case index
|
||||
|
||||
static let rootPath = "rooms"
|
||||
|
||||
public static let router = OneOf {
|
||||
Route(.case(Self.form)) {
|
||||
Path {
|
||||
rootPath
|
||||
"create"
|
||||
}
|
||||
Method.get
|
||||
}
|
||||
Route(.case(Self.index)) {
|
||||
Path { rootPath }
|
||||
Method.get
|
||||
}
|
||||
|
||||
53
Sources/Styleguide/Buttons.swift
Normal file
53
Sources/Styleguide/Buttons.swift
Normal file
@@ -0,0 +1,53 @@
|
||||
import Elementary
|
||||
|
||||
public struct SubmitButton: HTML, Sendable {
|
||||
let title: String
|
||||
let type: HTMLAttribute<HTMLTag.button>.ButtonType
|
||||
|
||||
public init(
|
||||
title: String = "Submit",
|
||||
type: HTMLAttribute<HTMLTag.button>.ButtonType = .submit
|
||||
) {
|
||||
self.title = title
|
||||
self.type = type
|
||||
}
|
||||
|
||||
public var body: some HTML<HTMLTag.button> {
|
||||
button(
|
||||
.class(
|
||||
"""
|
||||
text-white font-bold text-xl bg-blue-500 hover:bg-blue-600 px-4 py-2 rounded-lg shadow-lg
|
||||
"""
|
||||
),
|
||||
.type(type)
|
||||
) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct CancelButton: HTML, Sendable {
|
||||
let title: String
|
||||
let type: HTMLAttribute<HTMLTag.button>.ButtonType
|
||||
|
||||
public init(
|
||||
title: String = "Cancel",
|
||||
type: HTMLAttribute<HTMLTag.button>.ButtonType = .button
|
||||
) {
|
||||
self.title = title
|
||||
self.type = type
|
||||
}
|
||||
|
||||
public var body: some HTML<HTMLTag.button> {
|
||||
button(
|
||||
.class(
|
||||
"""
|
||||
text-white font-bold text-xl bg-red-500 hover:bg-red-600 px-4 py-2 rounded-lg shadow-lg
|
||||
"""
|
||||
),
|
||||
.type(type)
|
||||
) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
30
Sources/Styleguide/HTMXExtensions.swift
Normal file
30
Sources/Styleguide/HTMXExtensions.swift
Normal file
@@ -0,0 +1,30 @@
|
||||
import Elementary
|
||||
import ElementaryHTMX
|
||||
import ManualDCore
|
||||
|
||||
extension HTMLAttribute.hx {
|
||||
@Sendable
|
||||
public static func get(route: SiteRoute.View) -> HTMLAttribute {
|
||||
get(SiteRoute.View.router.path(for: route))
|
||||
}
|
||||
|
||||
@Sendable
|
||||
public static func patch(route: SiteRoute.View) -> HTMLAttribute {
|
||||
patch(SiteRoute.View.router.path(for: route))
|
||||
}
|
||||
|
||||
@Sendable
|
||||
public static func post(route: SiteRoute.View) -> HTMLAttribute {
|
||||
post(SiteRoute.View.router.path(for: route))
|
||||
}
|
||||
|
||||
@Sendable
|
||||
public static func put(route: SiteRoute.View) -> HTMLAttribute {
|
||||
put(SiteRoute.View.router.path(for: route))
|
||||
}
|
||||
|
||||
// @Sendable
|
||||
// static func delete(route: SiteRoute.Api) -> HTMLAttribute {
|
||||
// delete(SiteRoute.Api.router.path(for: route))
|
||||
// }
|
||||
}
|
||||
44
Sources/Styleguide/Icon.swift
Normal file
44
Sources/Styleguide/Icon.swift
Normal file
@@ -0,0 +1,44 @@
|
||||
import Elementary
|
||||
|
||||
public struct Icon: HTML, Sendable {
|
||||
|
||||
let icon: String
|
||||
|
||||
public init(icon: String) {
|
||||
self.icon = icon
|
||||
}
|
||||
|
||||
public var body: some HTML {
|
||||
i(.data("lucide", value: icon)) {}
|
||||
}
|
||||
}
|
||||
|
||||
extension Icon {
|
||||
|
||||
public init(_ icon: Key) {
|
||||
self.init(icon: icon.icon)
|
||||
}
|
||||
|
||||
public enum Key: String {
|
||||
|
||||
case circlePlus
|
||||
case close
|
||||
case doorClosed
|
||||
case mapPin
|
||||
case rulerDimensionLine
|
||||
case squareFunction
|
||||
case wind
|
||||
|
||||
var icon: String {
|
||||
switch self {
|
||||
case .circlePlus: return "circle-plus"
|
||||
case .close: return "x"
|
||||
case .doorClosed: return "door-closed"
|
||||
case .mapPin: return "map-pin"
|
||||
case .rulerDimensionLine: return "ruler-dimension-line"
|
||||
case .squareFunction: return "square-function"
|
||||
case .wind: return rawValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
45
Sources/Styleguide/Input.swift
Normal file
45
Sources/Styleguide/Input.swift
Normal file
@@ -0,0 +1,45 @@
|
||||
import Elementary
|
||||
|
||||
public struct Input: HTML, Sendable {
|
||||
let id: String
|
||||
let name: String?
|
||||
let placeholder: String
|
||||
|
||||
public init(
|
||||
id: String,
|
||||
name: String? = nil,
|
||||
placeholder: String
|
||||
) {
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.placeholder = placeholder
|
||||
}
|
||||
|
||||
public var body: some HTML<HTMLTag.input> {
|
||||
input(
|
||||
.id(id), .name(name ?? id), .placeholder(placeholder),
|
||||
.class(
|
||||
"""
|
||||
w-full rounded-md bg-white px-3 py-1.5 text-slate-900 outline-1
|
||||
-outline-offset-1 outline-slate-300 focus:outline focus:-outline-offset-2
|
||||
focus:outline-indigo-600 invalid:border-red-500 out-of-range:border-red-500
|
||||
"""
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension HTMLAttribute where Tag == HTMLTag.input {
|
||||
|
||||
public static func max(_ value: String) -> Self {
|
||||
.init(name: "max", value: value)
|
||||
}
|
||||
|
||||
public static func min(_ value: String) -> Self {
|
||||
.init(name: "min", value: value)
|
||||
}
|
||||
|
||||
public static func step(_ value: String) -> Self {
|
||||
.init(name: "step", value: value)
|
||||
}
|
||||
}
|
||||
18
Sources/Styleguide/Row.swift
Normal file
18
Sources/Styleguide/Row.swift
Normal file
@@ -0,0 +1,18 @@
|
||||
import Elementary
|
||||
|
||||
public struct Row<T: HTML>: HTML, Sendable where T: Sendable {
|
||||
|
||||
let inner: T
|
||||
|
||||
public init(
|
||||
@HTMLBuilder _ body: () -> T
|
||||
) {
|
||||
self.inner = body()
|
||||
}
|
||||
|
||||
public var body: some HTML<HTMLTag.div> {
|
||||
div(.class("flex justify-between")) {
|
||||
inner
|
||||
}
|
||||
}
|
||||
}
|
||||
29
Sources/Styleguide/SVG.swift
Normal file
29
Sources/Styleguide/SVG.swift
Normal file
@@ -0,0 +1,29 @@
|
||||
import Elementary
|
||||
|
||||
public struct SVG: HTML, Sendable {
|
||||
|
||||
let key: Key
|
||||
|
||||
public init(_ key: Key) {
|
||||
self.key = key
|
||||
}
|
||||
|
||||
public var body: some HTML {
|
||||
HTMLRaw(key.svg)
|
||||
}
|
||||
}
|
||||
|
||||
extension SVG {
|
||||
public enum Key: Sendable {
|
||||
case close
|
||||
|
||||
var svg: String {
|
||||
switch self {
|
||||
case .close:
|
||||
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>
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,8 +37,13 @@ extension SiteRoute.View.RoomRoute {
|
||||
func renderView(isHtmxRequest: Bool) async throws -> AnySendableHTML {
|
||||
switch self {
|
||||
case .form:
|
||||
// TODO: Check that it's an htmx request.
|
||||
return RoomForm()
|
||||
case .index:
|
||||
return MainPage {
|
||||
RoomTable(rooms: Room.mocks)
|
||||
div {
|
||||
RoomTable(rooms: Room.mocks)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,97 +1,63 @@
|
||||
import Dependencies
|
||||
import Elementary
|
||||
import ElementaryHTMX
|
||||
import Foundation
|
||||
import ManualDCore
|
||||
import Styleguide
|
||||
|
||||
// TODO: Need to hold the project ID in hidden input field.
|
||||
struct RoomForm: HTML, Sendable {
|
||||
|
||||
var body: some HTML {
|
||||
div(.class("mx-10 my-10")) {
|
||||
h1(.class("text-3xl font-bold pb-6")) { "Rooms " }
|
||||
div(
|
||||
.class(
|
||||
"fixed top-20 z-50 w-1/2 mx-[20vw] my-10 bg-gray-700 rounded-lg shadow-lg p-4"
|
||||
),
|
||||
.id("roomForm")
|
||||
) {
|
||||
h1(.class("text-3xl font-bold pb-6")) { "New Room" }
|
||||
form(
|
||||
.class(
|
||||
"""
|
||||
grid md:grid-cols-3 gap-4
|
||||
space-y-4
|
||||
"""
|
||||
)
|
||||
) {
|
||||
div(.class("col-span-1")) {
|
||||
div {
|
||||
label(.for("name")) { "Name:" }
|
||||
}
|
||||
input(
|
||||
.type(.text), .name("name"), .id("name"), .placeholder("Room Name"), .required,
|
||||
.autofocus
|
||||
)
|
||||
div {
|
||||
label(.for("name")) { "Name:" }
|
||||
Input(id: "name", placeholder: "Room Name")
|
||||
.attributes(.type(.text), .required, .autofocus)
|
||||
}
|
||||
div(.class("col-span-1")) {
|
||||
div {
|
||||
label(.for("heatingLoad")) { "Heating Load:" }
|
||||
}
|
||||
input(
|
||||
.type(.number), .name("heatingLoad"), .id("heatingLoad"), .placeholder("Heating Load"),
|
||||
.required
|
||||
)
|
||||
div {
|
||||
label(.for("heatingLoad")) { "Heating Load:" }
|
||||
Input(id: "heatingLoad", placeholder: "Heating Load")
|
||||
.attributes(.type(.number), .required, .min("0"))
|
||||
}
|
||||
div(.class("col-span-1")) {
|
||||
div {
|
||||
label(.for("coolingLoad")) { "Cooling Load:" }
|
||||
}
|
||||
input(
|
||||
.type(.number), .name("coolingLoad"), .id("coolingLoad"), .placeholder("Cooling Load"),
|
||||
.required
|
||||
)
|
||||
div {
|
||||
label(.for("coolingLoad")) { "Cooling Load:" }
|
||||
Input(id: "coolingLoad", placeholder: "Cooling Load")
|
||||
.attributes(.type(.number), .required, .min("0"))
|
||||
}
|
||||
div {
|
||||
label(.for("registerCount")) { "Registers:" }
|
||||
Input(id: "registerCount", placeholder: "Register Count")
|
||||
.attributes(.type(.number), .required, .value("1"), .min("1"))
|
||||
}
|
||||
Row {
|
||||
// Force button to the right, probably a better way.
|
||||
div {}
|
||||
div(.class("space-x-4")) {
|
||||
CancelButton()
|
||||
.attributes(
|
||||
.hx.get(route: .room(.index)),
|
||||
.hx.target("body"),
|
||||
.hx.swap(.outerHTML)
|
||||
)
|
||||
SubmitButton()
|
||||
}
|
||||
}
|
||||
.attributes(.class("py-4"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RoomTable: HTML, Sendable {
|
||||
let rooms: [Room]
|
||||
|
||||
var body: some HTML {
|
||||
div(.class("m-10")) {
|
||||
h1(.class("text-3xl font-bold")) { "Rooms" }
|
||||
table(
|
||||
.id("rooms"),
|
||||
.class(
|
||||
"w-full border-collapse border border-gray-200 table-fixed"
|
||||
)
|
||||
) {
|
||||
thead { tableHeader }
|
||||
tbody {
|
||||
Rows(rooms: rooms)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var tableHeader: some HTML<HTMLTag.tr> {
|
||||
tr {
|
||||
th(.class("border border-gray-200 text-xl font-bold")) { "Name" }
|
||||
th(.class("border border-gray-200 text-xl font-bold")) { "Heating Load" }
|
||||
th(.class("border border-gray-200 text-xl font-bold")) { "Cooling Total" }
|
||||
th(.class("border border-gray-200 text-xl font-bold")) { "Cooling Sensible" }
|
||||
th(.class("border border-gray-200 text-xl font-bold")) { "Register Count" }
|
||||
}
|
||||
}
|
||||
|
||||
private struct Rows: HTML, Sendable {
|
||||
let rooms: [Room]
|
||||
|
||||
var body: some HTML {
|
||||
for room in rooms {
|
||||
tr {
|
||||
td(.class("border border-gray-200 p-2")) { room.name }
|
||||
td(.class("border border-gray-200 p-2")) { "\(room.heatingLoad)" }
|
||||
td(.class("border border-gray-200 p-2")) { "\(room.coolingLoad.total)" }
|
||||
td(.class("border border-gray-200 p-2")) { "\(room.coolingLoad.sensible)" }
|
||||
td(.class("border border-gray-200 p-2")) { "\(room.registerCount)" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
71
Sources/ViewController/Views/Rooms/RoomTable.swift
Normal file
71
Sources/ViewController/Views/Rooms/RoomTable.swift
Normal file
@@ -0,0 +1,71 @@
|
||||
import Dependencies
|
||||
import Elementary
|
||||
import ElementaryHTMX
|
||||
import Foundation
|
||||
import ManualDCore
|
||||
import Styleguide
|
||||
|
||||
struct RoomTable: HTML, Sendable {
|
||||
let rooms: [Room]
|
||||
|
||||
var body: some HTML {
|
||||
div(.class("m-10")) {
|
||||
h1(.class("text-3xl font-bold")) { "Rooms" }
|
||||
table(
|
||||
.id("rooms"),
|
||||
.class(
|
||||
"w-full border-collapse border border-gray-200 table-fixed"
|
||||
)
|
||||
) {
|
||||
thead { tableHeader }
|
||||
tbody {
|
||||
Rows(rooms: rooms)
|
||||
}
|
||||
}
|
||||
div(.id("roomForm")) {}
|
||||
}
|
||||
}
|
||||
|
||||
private var tableHeader: some HTML<HTMLTag.tr> {
|
||||
tr {
|
||||
th(.class("border border-gray-200 text-xl font-bold")) { "Name" }
|
||||
th(.class("border border-gray-200 text-xl font-bold")) { "Heating Load" }
|
||||
th(.class("border border-gray-200 text-xl font-bold")) { "Cooling Total" }
|
||||
th(.class("border border-gray-200 text-xl font-bold")) { "Cooling Sensible" }
|
||||
th(.class("border border-gray-200 text-xl font-bold")) { "Register Count" }
|
||||
th(.class("border border-gray-200 text-xl font-bold")) {
|
||||
div(.class("flex justify-between")) {
|
||||
div {}
|
||||
button(
|
||||
.class("px-2"),
|
||||
.hx.get(route: .room(.form)),
|
||||
.hx.target("#roomForm"),
|
||||
.hx.swap(.outerHTML)
|
||||
) {
|
||||
Icon(.circlePlus)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct Rows: HTML, Sendable {
|
||||
let rooms: [Room]
|
||||
|
||||
var body: some HTML {
|
||||
for room in rooms {
|
||||
tr {
|
||||
td(.class("border border-gray-200 p-2")) { room.name }
|
||||
td(.class("border border-gray-200 p-2")) { "\(room.heatingLoad)" }
|
||||
td(.class("border border-gray-200 p-2")) { "\(room.coolingLoad.total)" }
|
||||
td(.class("border border-gray-200 p-2")) { "\(room.coolingLoad.sensible)" }
|
||||
td(.class("border border-gray-200 p-2")) { "\(room.registerCount)" }
|
||||
td(.class("border border-gray-200 p-2")) {
|
||||
// TODO: Add edit button.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,28 @@
|
||||
import Elementary
|
||||
import Styleguide
|
||||
|
||||
// TODO: Need to add active to sidebar links.
|
||||
struct Sidebar: HTML {
|
||||
|
||||
var body: some HTML {
|
||||
aside(
|
||||
.class(
|
||||
"""
|
||||
h-screen sticky top-0 border-r-3 border-gray-800 bg-gray-100 shadow
|
||||
h-screen sticky top-0 min-w-[280px] flex-none border border-r-3 border-gray-800 bg-gray-100 shadow
|
||||
"""
|
||||
)
|
||||
) {
|
||||
row(title: "Project", icon: "map-pin", href: "/projects")
|
||||
row(title: "Rooms", icon: "door-closed", href: "/rooms")
|
||||
row(title: "Equivalent Lengths", icon: "ruler-dimension-line", href: "#")
|
||||
row(title: "Friction Rate", icon: "square-function", href: "#")
|
||||
row(title: "Duct Sizes", icon: "wind", href: "#")
|
||||
row(title: "Project", icon: .mapPin, href: "/projects")
|
||||
row(title: "Rooms", icon: .doorClosed, href: "/rooms")
|
||||
row(title: "Equivalent Lengths", icon: .rulerDimensionLine, href: "#")
|
||||
row(title: "Friction Rate", icon: .squareFunction, href: "#")
|
||||
row(title: "Duct Sizes", icon: .wind, href: "#")
|
||||
}
|
||||
}
|
||||
|
||||
private func row(
|
||||
title: String,
|
||||
icon: String,
|
||||
icon: Icon.Key,
|
||||
href: String
|
||||
) -> some HTML {
|
||||
a(
|
||||
@@ -31,10 +33,8 @@ struct Sidebar: HTML {
|
||||
),
|
||||
.href(href)
|
||||
) {
|
||||
i(.data("lucide", value: icon)) {}
|
||||
p(
|
||||
.class("text-xl font-bold")
|
||||
) {
|
||||
Icon(icon)
|
||||
span(.class("text-xl font-bold")) {
|
||||
title
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM swift:6.2-noble
|
||||
FROM docker.io/swift:6.2-noble
|
||||
|
||||
# Make sure all system packages are up to date, and install only essential packages.
|
||||
RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
touch .build/browser-dev-sync
|
||||
browser-sync start -p 0.0.0.0:8080 --ws --no-open &
|
||||
# browser-sync start --proxy localhost:8080 --ws -w --no-notify &
|
||||
watchexec -w Sources -e .swift -r 'swift build --product App && touch .build/browser-dev-sync' &
|
||||
watchexec -w .build/browser-dev-sync --ignore-nothing -r '.build/debug/App'
|
||||
watchexec -w .build/browser-dev-sync -r 'swift run App'
|
||||
|
||||
Reference in New Issue
Block a user