WIP: Mostly done with pdf client, need to add tests.
This commit is contained in:
@@ -105,6 +105,8 @@ let package = Package(
|
|||||||
.target(
|
.target(
|
||||||
name: "PdfClient",
|
name: "PdfClient",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
|
.target(name: "EnvClient"),
|
||||||
|
.target(name: "FileClient"),
|
||||||
.target(name: "ManualDCore"),
|
.target(name: "ManualDCore"),
|
||||||
.product(name: "Dependencies", package: "swift-dependencies"),
|
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||||
.product(name: "DependenciesMacros", package: "swift-dependencies"),
|
.product(name: "DependenciesMacros", package: "swift-dependencies"),
|
||||||
@@ -117,6 +119,7 @@ let package = Package(
|
|||||||
.target(name: "DatabaseClient"),
|
.target(name: "DatabaseClient"),
|
||||||
.target(name: "ManualDClient"),
|
.target(name: "ManualDClient"),
|
||||||
.target(name: "PdfClient"),
|
.target(name: "PdfClient"),
|
||||||
|
.product(name: "Vapor", package: "vapor"),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
.target(
|
.target(
|
||||||
|
|||||||
12
Public/css/htmx.css
Normal file
12
Public/css/htmx.css
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
.htmx-indicator {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.htmx-request .htmx-indicator {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.htmx-request.htmx-indicator {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -2,4 +2,3 @@
|
|||||||
@plugin "daisyui" {
|
@plugin "daisyui" {
|
||||||
themes: all;
|
themes: all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8548,10 +8548,6 @@
|
|||||||
.italic {
|
.italic {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
.lining-nums {
|
|
||||||
--tw-numeric-figure: lining-nums;
|
|
||||||
font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);
|
|
||||||
}
|
|
||||||
.tabular-nums {
|
.tabular-nums {
|
||||||
--tw-numeric-spacing: tabular-nums;
|
--tw-numeric-spacing: tabular-nums;
|
||||||
font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);
|
font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);
|
||||||
|
|||||||
63
Public/js/htmx-download.js
Normal file
63
Public/js/htmx-download.js
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
// Copied from: https://github.com/dakixr/htmx-download/blob/main/htmx-download.js
|
||||||
|
htmx.defineExtension('htmx-download', {
|
||||||
|
onEvent: function(name, evt) {
|
||||||
|
|
||||||
|
if (name === 'htmx:beforeRequest') {
|
||||||
|
// Set the responseType to 'arraybuffer' to handle binary data
|
||||||
|
evt.detail.xhr.responseType = 'arraybuffer';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'htmx:beforeSwap') {
|
||||||
|
const xhr = evt.detail.xhr;
|
||||||
|
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
// Parse headers
|
||||||
|
const headers = {};
|
||||||
|
const headerStr = xhr.getAllResponseHeaders();
|
||||||
|
const headerArr = headerStr.trim().split(/[\r\n]+/);
|
||||||
|
headerArr.forEach((line) => {
|
||||||
|
const parts = line.split(": ");
|
||||||
|
const header = parts.shift().toLowerCase();
|
||||||
|
const value = parts.join(": ");
|
||||||
|
headers[header] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Extract filename
|
||||||
|
let filename = 'downloaded_file.xlsx';
|
||||||
|
if (headers['content-disposition']) {
|
||||||
|
const filenameMatch = headers['content-disposition'].match(/filename\*?=(?:UTF-8'')?"?([^;\n"]+)/i);
|
||||||
|
if (filenameMatch && filenameMatch[1]) {
|
||||||
|
filename = decodeURIComponent(filenameMatch[1].replace(/['"]/g, ''));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine MIME type
|
||||||
|
const mimetype = headers['content-type'] || 'application/octet-stream';
|
||||||
|
|
||||||
|
// Create Blob
|
||||||
|
const blob = new Blob([xhr.response], { type: mimetype });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
// Trigger download
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.style.display = "none";
|
||||||
|
link.href = url;
|
||||||
|
link.download = filename;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
setTimeout(() => {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
link.remove();
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
console.warn(`[htmx-download] Unexpected response status: ${xhr.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent htmx from swapping content
|
||||||
|
evt.detail.shouldSwap = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -117,37 +117,38 @@ extension DuctSizes: Content {}
|
|||||||
// FIX: Move
|
// FIX: Move
|
||||||
func handlePdf(_ projectID: Project.ID, on request: Request) async throws -> Response {
|
func handlePdf(_ projectID: Project.ID, on request: Request) async throws -> Response {
|
||||||
@Dependency(\.projectClient) var projectClient
|
@Dependency(\.projectClient) var projectClient
|
||||||
|
return try await projectClient.generatePdf(projectID, request.fileio)
|
||||||
|
|
||||||
let html = try await projectClient.toHTML(projectID)
|
// let html = try await projectClient.toHTML(projectID)
|
||||||
let url = "/tmp/\(projectID)"
|
// let url = "/tmp/\(projectID)"
|
||||||
try await request.fileio.writeFile(.init(string: html.render()), at: "\(url).html")
|
// try await request.fileio.writeFile(.init(string: html.render()), at: "\(url).html")
|
||||||
|
//
|
||||||
let process = Process()
|
// let process = Process()
|
||||||
let standardInput = Pipe()
|
// let standardInput = Pipe()
|
||||||
let standardOutput = Pipe()
|
// let standardOutput = Pipe()
|
||||||
process.standardInput = standardInput
|
// process.standardInput = standardInput
|
||||||
process.standardOutput = standardOutput
|
// process.standardOutput = standardOutput
|
||||||
process.executableURL = URL(fileURLWithPath: "/bin/pandoc")
|
// process.executableURL = URL(fileURLWithPath: "/bin/pandoc")
|
||||||
process.arguments = [
|
// process.arguments = [
|
||||||
"\(url).html",
|
// "\(url).html",
|
||||||
"--pdf-engine=weasyprint",
|
// "--pdf-engine=weasyprint",
|
||||||
"--from=html",
|
// "--from=html",
|
||||||
"--css=Public/css/pdf.css",
|
// "--css=Public/css/pdf.css",
|
||||||
"-o", "\(url).pdf",
|
// "-o", "\(url).pdf",
|
||||||
]
|
// ]
|
||||||
try process.run()
|
// try process.run()
|
||||||
process.waitUntilExit()
|
// process.waitUntilExit()
|
||||||
|
//
|
||||||
let response = try await request.fileio.asyncStreamFile(at: "\(url).pdf", mediaType: .pdf) { _ in
|
// let response = try await request.fileio.asyncStreamFile(at: "\(url).pdf", mediaType: .pdf) { _ in
|
||||||
// Remove files here.
|
// // Remove files here.
|
||||||
try FileManager.default.removeItem(atPath: "\(url).pdf")
|
// try FileManager.default.removeItem(atPath: "\(url).pdf")
|
||||||
try FileManager.default.removeItem(atPath: "\(url).html")
|
// try FileManager.default.removeItem(atPath: "\(url).html")
|
||||||
}
|
// }
|
||||||
response.headers.replaceOrAdd(name: .contentType, value: "application/octet-stream")
|
// response.headers.replaceOrAdd(name: .contentType, value: "application/octet-stream")
|
||||||
response.headers.replaceOrAdd(
|
// response.headers.replaceOrAdd(
|
||||||
name: .contentDisposition, value: "attachment; filename=Duct-Calc.pdf"
|
// name: .contentDisposition, value: "attachment; filename=Duct-Calc.pdf"
|
||||||
)
|
// )
|
||||||
return response
|
// return response
|
||||||
}
|
}
|
||||||
|
|
||||||
@Sendable
|
@Sendable
|
||||||
|
|||||||
@@ -16,13 +16,19 @@ extension DependencyValues {
|
|||||||
|
|
||||||
@DependencyClient
|
@DependencyClient
|
||||||
struct EnvClient: Sendable {
|
struct EnvClient: Sendable {
|
||||||
|
|
||||||
public var env: @Sendable () throws -> EnvVars
|
public var env: @Sendable () throws -> EnvVars
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Holds values defined in the process environment that are needed.
|
||||||
|
///
|
||||||
|
/// These are generally loaded from a `.env` file, but also have default values,
|
||||||
|
/// if not found.
|
||||||
public struct EnvVars: Codable, Equatable, Sendable {
|
public struct EnvVars: Codable, Equatable, Sendable {
|
||||||
|
|
||||||
|
/// The path to the pandoc executable on the system, used to generate pdf's.
|
||||||
public let pandocPath: String
|
public let pandocPath: String
|
||||||
|
|
||||||
|
/// The pdf engine to use with pandoc when creating pdf's.
|
||||||
public let pdfEngine: String
|
public let pdfEngine: String
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
@@ -42,6 +48,7 @@ public struct EnvVars: Codable, Equatable, Sendable {
|
|||||||
|
|
||||||
extension EnvClient: DependencyKey {
|
extension EnvClient: DependencyKey {
|
||||||
static let testValue = Self()
|
static let testValue = Self()
|
||||||
|
|
||||||
static let liveValue = Self(env: {
|
static let liveValue = Self(env: {
|
||||||
// Convert default values into a dictionary.
|
// Convert default values into a dictionary.
|
||||||
let defaults =
|
let defaults =
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import Dependencies
|
import Dependencies
|
||||||
import DependenciesMacros
|
import DependenciesMacros
|
||||||
import Elementary
|
import Elementary
|
||||||
|
import EnvClient
|
||||||
|
import FileClient
|
||||||
|
import Foundation
|
||||||
import ManualDCore
|
import ManualDCore
|
||||||
|
|
||||||
extension DependencyValues {
|
extension DependencyValues {
|
||||||
@@ -14,6 +17,13 @@ extension DependencyValues {
|
|||||||
@DependencyClient
|
@DependencyClient
|
||||||
public struct PdfClient: Sendable {
|
public struct PdfClient: Sendable {
|
||||||
public var html: @Sendable (Request) async throws -> (any HTML & Sendable)
|
public var html: @Sendable (Request) async throws -> (any HTML & Sendable)
|
||||||
|
public var generatePdf: @Sendable (Project.ID, any HTML & Sendable) async throws -> Response
|
||||||
|
|
||||||
|
public func generatePdf(request: Request) async throws -> Response {
|
||||||
|
let html = try await self.html(request)
|
||||||
|
return try await self.generatePdf(request.project.id, html)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension PdfClient: DependencyKey {
|
extension PdfClient: DependencyKey {
|
||||||
@@ -22,6 +32,33 @@ extension PdfClient: DependencyKey {
|
|||||||
public static let liveValue = Self(
|
public static let liveValue = Self(
|
||||||
html: { request in
|
html: { request in
|
||||||
request.toHTML()
|
request.toHTML()
|
||||||
|
},
|
||||||
|
generatePdf: { projectID, html in
|
||||||
|
@Dependency(\.fileClient) var fileClient
|
||||||
|
@Dependency(\.env) var env
|
||||||
|
|
||||||
|
let envVars = try env()
|
||||||
|
let baseUrl = "/tmp/\(projectID)"
|
||||||
|
try await fileClient.writeFile(html.render(), "\(baseUrl).html")
|
||||||
|
|
||||||
|
let process = Process()
|
||||||
|
let standardInput = Pipe()
|
||||||
|
let standardOutput = Pipe()
|
||||||
|
process.standardInput = standardInput
|
||||||
|
process.standardOutput = standardOutput
|
||||||
|
process.executableURL = URL(fileURLWithPath: envVars.pandocPath)
|
||||||
|
process.arguments = [
|
||||||
|
"\(baseUrl).html",
|
||||||
|
"--pdf-engine=\(envVars.pdfEngine)",
|
||||||
|
"--from=html",
|
||||||
|
"--css=Public/css/pdf.css",
|
||||||
|
"--output=\(baseUrl).pdf",
|
||||||
|
]
|
||||||
|
try process.run()
|
||||||
|
process.waitUntilExit()
|
||||||
|
|
||||||
|
return .init(htmlPath: "\(baseUrl).html", pdfPath: "\(baseUrl).pdf")
|
||||||
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -40,6 +77,10 @@ extension PdfClient {
|
|||||||
public let frictionRate: FrictionRate
|
public let frictionRate: FrictionRate
|
||||||
public let projectSHR: Double
|
public let projectSHR: Double
|
||||||
|
|
||||||
|
var totalEquivalentLength: Double {
|
||||||
|
maxReturnTEL.totalEquivalentLength + maxSupplyTEL.totalEquivalentLength
|
||||||
|
}
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
project: Project,
|
project: Project,
|
||||||
rooms: [Room],
|
rooms: [Room],
|
||||||
@@ -62,6 +103,17 @@ extension PdfClient {
|
|||||||
self.projectSHR = projectSHR
|
self.projectSHR = projectSHR
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct Response: Equatable, Sendable {
|
||||||
|
|
||||||
|
public let htmlPath: String
|
||||||
|
public let pdfPath: String
|
||||||
|
|
||||||
|
public init(htmlPath: String, pdfPath: String) {
|
||||||
|
self.htmlPath = htmlPath
|
||||||
|
self.pdfPath = pdfPath
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ struct PdfDocument: HTMLDocument {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
div(.class("section")) {
|
div(.class("section")) {
|
||||||
h2 { "Duct Sizes" }
|
h2 { "Duct Sizes" }
|
||||||
DuctSizesTable(rooms: request.ductSizes.rooms)
|
DuctSizesTable(rooms: request.ductSizes.rooms)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import DependenciesMacros
|
|||||||
import Elementary
|
import Elementary
|
||||||
import ManualDClient
|
import ManualDClient
|
||||||
import ManualDCore
|
import ManualDCore
|
||||||
|
import Vapor
|
||||||
|
|
||||||
extension DependencyValues {
|
extension DependencyValues {
|
||||||
public var projectClient: ProjectClient {
|
public var projectClient: ProjectClient {
|
||||||
@@ -29,8 +30,10 @@ public struct ProjectClient: Sendable {
|
|||||||
public var frictionRate: @Sendable (Project.ID) async throws -> FrictionRateResponse
|
public var frictionRate: @Sendable (Project.ID) async throws -> FrictionRateResponse
|
||||||
|
|
||||||
// FIX: Name to something to do with generating a pdf, just experimenting now.
|
// FIX: Name to something to do with generating a pdf, just experimenting now.
|
||||||
public var toMarkdown: @Sendable (Project.ID) async throws -> String
|
// public var toMarkdown: @Sendable (Project.ID) async throws -> String
|
||||||
public var toHTML: @Sendable (Project.ID) async throws -> (any HTML & Sendable)
|
// public var toHTML: @Sendable (Project.ID) async throws -> (any HTML & Sendable)
|
||||||
|
|
||||||
|
public var generatePdf: @Sendable (Project.ID, FileIO) async throws -> Response
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ProjectClient: TestDependencyKey {
|
extension ProjectClient: TestDependencyKey {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import DatabaseClient
|
import DatabaseClient
|
||||||
import Dependencies
|
import Dependencies
|
||||||
|
import FileClient
|
||||||
import Logging
|
import Logging
|
||||||
import ManualDClient
|
import ManualDClient
|
||||||
import ManualDCore
|
import ManualDCore
|
||||||
@@ -11,6 +12,7 @@ extension ProjectClient: DependencyKey {
|
|||||||
@Dependency(\.database) var database
|
@Dependency(\.database) var database
|
||||||
@Dependency(\.manualD) var manualD
|
@Dependency(\.manualD) var manualD
|
||||||
@Dependency(\.pdfClient) var pdfClient
|
@Dependency(\.pdfClient) var pdfClient
|
||||||
|
@Dependency(\.fileClient) var fileClient
|
||||||
|
|
||||||
return .init(
|
return .init(
|
||||||
calculateDuctSizes: { projectID in
|
calculateDuctSizes: { projectID in
|
||||||
@@ -35,11 +37,21 @@ extension ProjectClient: DependencyKey {
|
|||||||
frictionRate: { projectID in
|
frictionRate: { projectID in
|
||||||
try await manualD.frictionRate(projectID: projectID)
|
try await manualD.frictionRate(projectID: projectID)
|
||||||
},
|
},
|
||||||
toMarkdown: { projectID in
|
generatePdf: { projectID, fileIO in
|
||||||
try await pdfClient.markdown(database.makePdfRequest(projectID))
|
let pdfResponse = try await pdfClient.generatePdf(
|
||||||
},
|
request: database.makePdfRequest(projectID))
|
||||||
toHTML: { projectID in
|
|
||||||
try await pdfClient.html(database.makePdfRequest(projectID))
|
let response = try await fileIO.asyncStreamFile(at: pdfResponse.pdfPath) { _ in
|
||||||
|
try await fileClient.removeFile(pdfResponse.htmlPath)
|
||||||
|
try await fileClient.removeFile(pdfResponse.pdfPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
response.headers.replaceOrAdd(name: .contentType, value: "application/octet-stream")
|
||||||
|
response.headers.replaceOrAdd(
|
||||||
|
name: .contentDisposition, value: "attachment; filename=Duct-Calc.pdf"
|
||||||
|
)
|
||||||
|
|
||||||
|
return response
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -48,31 +60,6 @@ extension ProjectClient: DependencyKey {
|
|||||||
|
|
||||||
extension DatabaseClient {
|
extension DatabaseClient {
|
||||||
|
|
||||||
// fileprivate func makePdfRequest(_ projectID: Project.ID) async throws -> PdfClient.Request {
|
|
||||||
// @Dependency(\.manualD) var manualD
|
|
||||||
//
|
|
||||||
// guard let project = try await projects.get(projectID) else {
|
|
||||||
// throw ProjectClientError("Project not found. id: \(projectID)")
|
|
||||||
// }
|
|
||||||
// let frictionRateResponse = try await manualD.frictionRate(projectID: projectID)
|
|
||||||
// guard let frictionRate = frictionRateResponse.frictionRate else {
|
|
||||||
// throw ProjectClientError("Friction rate not found. id: \(projectID)")
|
|
||||||
// }
|
|
||||||
// let (ductSizes, sharedInfo, rooms) = try await calculateDuctSizes(projectID: projectID)
|
|
||||||
//
|
|
||||||
// return .init(
|
|
||||||
// project: project,
|
|
||||||
// rooms: rooms,
|
|
||||||
// componentLosses: frictionRateResponse.componentLosses,
|
|
||||||
// ductSizes: ductSizes,
|
|
||||||
// equipmentInfo: sharedInfo.equipmentInfo,
|
|
||||||
// maxSupplyTEL: sharedInfo.maxSupplyLength,
|
|
||||||
// maxReturnTEL: sharedInfo.maxReturnLenght,
|
|
||||||
// frictionRate: frictionRate,
|
|
||||||
// projectSHR: sharedInfo.projectSHR
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
|
|
||||||
fileprivate func makePdfRequest(_ projectID: Project.ID) async throws -> PdfClient.Request {
|
fileprivate func makePdfRequest(_ projectID: Project.ID) async throws -> PdfClient.Request {
|
||||||
@Dependency(\.manualD) var manualD
|
@Dependency(\.manualD) var manualD
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,6 @@ extension HTMLAttribute.hx {
|
|||||||
extension HTMLAttribute.hx {
|
extension HTMLAttribute.hx {
|
||||||
@Sendable
|
@Sendable
|
||||||
public static func indicator() -> HTMLAttribute {
|
public static func indicator() -> HTMLAttribute {
|
||||||
indicator(".hx-indicator")
|
indicator(".htmx-indicator")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -195,7 +195,9 @@ extension SiteRoute.View.ProjectRoute {
|
|||||||
case .pdf:
|
case .pdf:
|
||||||
// FIX: This should return a pdf to download or be wrapped in a
|
// FIX: This should return a pdf to download or be wrapped in a
|
||||||
// result view.
|
// result view.
|
||||||
return try! await projectClient.toHTML(projectID)
|
// return try! await projectClient.toHTML(projectID)
|
||||||
|
// This get's handled elsewhere because it returns a response, not a view.
|
||||||
|
fatalError()
|
||||||
case .rooms(let route):
|
case .rooms(let route):
|
||||||
return await route.renderView(on: request, projectID: projectID)
|
return await route.renderView(on: request, projectID: projectID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,11 +24,20 @@ struct DuctSizingView: HTML, Sendable {
|
|||||||
.attributes(.class("text-error font-bold italic mt-4"))
|
.attributes(.class("text-error font-bold italic mt-4"))
|
||||||
}
|
}
|
||||||
|
|
||||||
a(
|
div {
|
||||||
.class("btn btn-primary"),
|
button(
|
||||||
.href(route: .project(.detail(projectID, .pdf)))
|
.class("btn btn-primary"),
|
||||||
) {
|
.hx.get(route: .project(.detail(projectID, .pdf))),
|
||||||
"PDF"
|
.hx.ext("htmx-download"),
|
||||||
|
.hx.swap(.none),
|
||||||
|
.hx.indicator()
|
||||||
|
) {
|
||||||
|
span { "PDF" }
|
||||||
|
Indicator()
|
||||||
|
}
|
||||||
|
// div {
|
||||||
|
// Indicator()
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,8 +50,10 @@ public struct MainPage<Inner: HTML>: SendableHTMLDocument where Inner: Sendable
|
|||||||
meta(.content("1024"), .name("og:image:height"))
|
meta(.content("1024"), .name("og:image:height"))
|
||||||
meta(.content(keywords), .name(.keywords))
|
meta(.content(keywords), .name(.keywords))
|
||||||
script(.src("https://unpkg.com/htmx.org@2.0.8")) {}
|
script(.src("https://unpkg.com/htmx.org@2.0.8")) {}
|
||||||
|
script(.src("/js/htmx-download.js")) {}
|
||||||
script(.src("/js/main.js")) {}
|
script(.src("/js/main.js")) {}
|
||||||
link(.rel(.stylesheet), .href("/css/output.css"))
|
link(.rel(.stylesheet), .href("/css/output.css"))
|
||||||
|
link(.rel(.stylesheet), .href("/css/htmx.css"))
|
||||||
link(
|
link(
|
||||||
.rel(.icon),
|
.rel(.icon),
|
||||||
.href("/images/favicon.ico"),
|
.href("/images/favicon.ico"),
|
||||||
|
|||||||
Reference in New Issue
Block a user