WIP: Moves some common views to a Styleguide module, working on room table and form.

This commit is contained in:
2025-12-31 16:16:39 -05:00
parent c29e1acffe
commit 34bba7bdfc
15 changed files with 370 additions and 90 deletions

1
.gitignore vendored
View File

@@ -6,3 +6,4 @@ DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
.swift-version

View File

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

View File

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

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

View 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))
// }
}

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

View 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)
}
}

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

View 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>
"""
}
}
}
}

View File

@@ -37,11 +37,16 @@ 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 {
div {
RoomTable(rooms: Room.mocks)
}
}
}
}
}
private let mainPage: AnySendableHTML = {

View File

@@ -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(id: "name", placeholder: "Room Name")
.attributes(.type(.text), .required, .autofocus)
}
input(
.type(.text), .name("name"), .id("name"), .placeholder("Room Name"), .required,
.autofocus
)
}
div(.class("col-span-1")) {
div {
label(.for("heatingLoad")) { "Heating Load:" }
Input(id: "heatingLoad", placeholder: "Heating Load")
.attributes(.type(.number), .required, .min("0"))
}
input(
.type(.number), .name("heatingLoad"), .id("heatingLoad"), .placeholder("Heating Load"),
.required
)
}
div(.class("col-span-1")) {
div {
label(.for("coolingLoad")) { "Cooling Load:" }
Input(id: "coolingLoad", placeholder: "Cooling Load")
.attributes(.type(.number), .required, .min("0"))
}
input(
.type(.number), .name("coolingLoad"), .id("coolingLoad"), .placeholder("Cooling Load"),
.required
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)" }
}
}
}
}
}

View 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.
}
}
}
}
}
}

View File

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

View File

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

View File

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