feat: Adds styleguide, working on result view container.

This commit is contained in:
2025-02-26 17:08:13 -05:00
parent cce99ce5e9
commit a15e54e0e4
16 changed files with 569 additions and 122 deletions

View File

@@ -0,0 +1,19 @@
import Elementary
public struct SubmitButton: HTML, Sendable {
let label: String
public init(label: String) {
self.label = label
}
public var content: some HTML<HTMLTag.button> {
button(
.type(.submit),
.class("""
w-full \(bg: .blue) \(text: .yellow) font-bold py-3 rounded-md
hover:\(bg: .darkBlue) transition-colors
""")
) { label }
}
}

View File

@@ -0,0 +1,85 @@
/// Common tailwind colors used.
///
/// Use these with string interpolation in the class.
///
/// **Example:**
///
/// ```swift
/// div(.class("\(text: .yellow) \(border: .gray) \(bg: .blue)")) {
/// ...
/// }
/// ```
public enum Color {
public enum BackgroundColor: Sendable {
case blue
case darkBlue
case darkGray
case darkSlate
case slate
case yellow
var color: String {
switch self {
case .blue: return "bg-blue-500"
case .darkBlue: return "bg-blue-600"
case .darkGray: return "bg-gray-700"
case .darkSlate: return "bg-slate-700"
case .slate: return "bg-slate-300"
case .yellow: return "bg-yellow-300"
}
}
}
public enum BorderColor: Sendable {
case darkYellow
case gray
case yellow
var color: String {
switch self {
case .darkYellow: return "border-yellow-800"
case .gray: return "border-gray-300"
case .yellow: return "border-yellow-300"
}
}
}
public enum TextColor: Sendable {
case blue
case darkBlue
case darkGray
case gray
case green
case yellow
case white
var color: String {
switch self {
case .blue: return "text-blue-500"
case .darkBlue: return "text-blue-600"
case .darkGray: return "text-gray-700"
case .gray: return "text-gray-300"
case .green: return "text-green-600"
case .yellow: return "text-yellow-300"
case .white: return "text-white"
}
}
}
}
public extension String.StringInterpolation {
mutating func appendInterpolation(bg background: Color.BackgroundColor) {
appendInterpolation(background.color)
}
mutating func appendInterpolation(border: Color.BorderColor) {
appendInterpolation(border.color)
}
mutating func appendInterpolation(text: Color.TextColor) {
appendInterpolation(text.color)
}
}

View File

@@ -0,0 +1,84 @@
import Elementary
/// A styled header for a form element, which consists of an
/// svg image and label / name for the form.
///
public struct FormHeader: HTML, Sendable {
let label: String
let svg: SVGType
public init(
label: String,
svg: SVGType
) {
self.label = label
self.svg = svg
}
public var content: some HTML {
LabeledContent {
h2(.class("text-2xl font-extrabold dark:\(text: .white)")) { label }
} label: {
SVG(svg, color: .blue)
}
.attributes(.class("flex items-center gap-3 mb-6"))
}
}
/// A styled form input, does not contain the input type which is generally
/// added at the call site.
///
/// **Example:**
/// ```swift
/// Input(id: "email", placeholder: "Email")
/// .attributes(.type(.email))
/// ```
///
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 content: some HTML<HTMLTag.input> {
input(
.id(id), .placeholder(placeholder), .name(name ?? id),
.class("""
w-full px-4 py-2 border \(border: .gray) rounded-md focus:ring-2
focus:ring-yellow-800 focus:border-yellow-800 \(text: .darkGray) dark:\(text: .white)
""")
)
}
}
/// A style form input label.
public struct InputLabel<InputLabel: HTML>: HTML {
let forInputId: String
let inputLabel: InputLabel
public init(
for forInputId: String,
@HTMLBuilder label: () -> InputLabel
) {
self.forInputId = forInputId
self.inputLabel = label()
}
public var content: some HTML<HTMLTag.label> {
label(
.for(forInputId),
.class("block text-sm font-medium \(text: .darkGray) dark:\(text: .gray) mb-2")
) {
self.inputLabel
}
}
}
extension InputLabel: Sendable where InputLabel: Sendable {}

View File

@@ -0,0 +1,54 @@
import Elementary
public struct LabeledContent<Label: HTML, Body: HTML>: HTML {
let body: Body
let label: Label
public init(
@HTMLBuilder body: () -> Body,
@HTMLBuilder label: () -> Label
) {
self.body = body()
self.label = label()
}
public var content: some HTML<HTMLTag.div> {
div {
label
body
}
}
}
extension LabeledContent: Sendable where Label: Sendable, Body: Sendable {}
// MARK: - Forms
public extension LabeledContent where Label == InputLabel<HTMLText>, Body == Input {
init(
label: String,
input: () -> Body
) {
self.init {
input()
} label: {
InputLabel(for: input().id) { HTMLText(label) }
}
}
}
public extension LabeledContent where Label == InputLabel<HTMLText>, Body == _AttributedElement<Input> {
init(
label: String,
input: () -> Body
) {
self.init {
input()
} label: {
InputLabel(for: input().content.id) { HTMLText(label) }
}
}
}

View File

@@ -0,0 +1,20 @@
import Elementary
public struct ResultContainer<Body: HTML>: HTML {
let body: Body
public init(
@HTMLBuilder body: () -> Body
) {
self.body = body()
}
public var content: some HTML {
div(.class("mt-6 p-6 bg-blue-50 dark:bg-slate-400 rounded-lg")) {
h3(.class("text-xl font-semibold \(text: .darkGray) mb-4")) { "Results" }
body
}
}
}
extension ResultContainer: Sendable where Body: Sendable {}

View File

@@ -0,0 +1,158 @@
import Elementary
public struct SVG: HTML, Sendable {
let color: String
let size: SVGSize
let svg: SVGType
public init(
_ svg: SVGType,
color: String,
size: SVGSize = .init()
) {
self.svg = svg
self.size = size
self.color = color
}
public init(
_ svg: SVGType,
color: Color.TextColor,
size: SVGSize = .init()
) {
self.svg = svg
self.size = size
self.color = color.color
}
public var content: some HTML<HTMLTag.div> {
div(.class("block \(color)")) {
svg.html(size)
}
}
}
public struct SVGSize: Sendable {
let width: Int
let height: Int
public init(width: Int = 24, height: Int? = nil) {
self.width = width
self.height = height ?? width
}
}
public enum SVGType: Sendable {
case calculator
case exclamation
case menu
case thermometer
case toolbox
case wind
public func html(_ size: SVGSize) -> some HTML {
switch self {
case .calculator: return calculatorSvg(size: size)
case .exclamation: return exclamationSvg(size: size)
case .menu: return menuSvg(size: size)
case .thermometer: return thermometerSvg(size: size)
case .toolbox: return toolboxSvg(size: size)
case .wind: return windSvg(size: size)
}
}
}
// MARK: - SVGs
// swiftlint:disable line_length
// TODO: Requires attribution:
// Vectors and icons by <a href="https://dribbble.com/Laridae?ref=svgrepo.com" target="_blank">Laridae</a> in CC Attribution License via <a href="https://www.svgrepo.com/" target="_blank">SVG Repo</a>
// FIX: This doesn't work, but does as a file
private func toolboxSvg(size: SVGSize) -> HTMLRaw {
HTMLRaw("""
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="\(size.width)" height="\(size.height)" stroke="currentColor" viewBox="0 0 \(size.width) \(size.height)" class="icon" version="1.1" xmlns="http://www.w3.org/2000/svg" fill="#000000">
<g id="SVGRepo_bgCarrier" stroke="currentColor" stroke-width="2"/>
<g id="SVGRepo_tracerCarrier" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
<g id="SVGRepo_iconCarrier">
<path d="M619.52 194.56h35.84V128c0-5.632-4.608-10.24-10.24-10.24H378.88c-5.632 0-10.24 4.608-10.24 10.24v66.56h35.84v-30.72c0-5.632 4.608-10.24 10.24-10.24h194.56c5.632 0 10.24 4.608 10.24 10.24v30.72z" fill="#2C7FFF"/>
<path d="M184.32 209.92v20.48h655.36v-20.48H184.32z m-5.12-15.36h665.6c5.632 0 10.24 4.608 10.24 10.24v30.72c0 5.632-4.608 10.24-10.24 10.24H179.2c-5.632 0-10.24-4.608-10.24-10.24v-30.72c0-5.632 4.608-10.24 10.24-10.24zM240.64 890.88h51.2v-10.24H240.64v10.24z m-5.12-25.6h61.44c5.632 0 10.24 4.608 10.24 10.24v20.48c0 5.632-4.608 10.24-10.24 10.24H235.52c-5.632 0-10.24-4.608-10.24-10.24v-20.48c0-5.632 4.608-10.24 10.24-10.24zM737.28 890.88h51.2v-10.24h-51.2v10.24z m-5.12-25.6h61.44c5.632 0 10.24 4.608 10.24 10.24v20.48c0 5.632-4.608 10.24-10.24 10.24h-61.44c-5.632 0-10.24-4.608-10.24-10.24v-20.48c0-5.632 4.608-10.24 10.24-10.24z" fill=""/>
<path d="M199.68 343.04h122.88c5.632 0 10.24 4.608 10.24 10.24v81.92c0 5.632-4.608 10.24-10.24 10.24H199.68c-5.632 0-10.24-4.608-10.24-10.24V353.28c0-5.632 4.608-10.24 10.24-10.24zM701.44 343.04h122.88c5.632 0 10.24 4.608 10.24 10.24v81.92c0 5.632-4.608 10.24-10.24 10.24h-122.88c-5.632 0-10.24-4.608-10.24-10.24V353.28c0-5.632 4.608-10.24 10.24-10.24z" fill="#2C7FFF"/>
<path d="M873.472 276.48H148.48c-11.264 1.024-19.456 10.752-18.432 22.016l48.128 522.24c1.024 10.752 9.728 18.432 20.48 18.432h626.688c10.752 0 19.456-8.192 20.48-18.432l48.64-522.24v-2.048c-0.512-10.752-9.728-19.968-20.992-19.968zM150.528 291.84h722.944c3.072 0 5.12 2.048 5.12 5.632L870.4 384h-35.84v-30.72c0-5.632-4.608-10.24-10.24-10.24h-122.88c-5.632 0-10.24 4.608-10.24 10.24v30.72H332.8v-30.72c0-5.632-4.608-10.24-10.24-10.24H199.68c-5.632 0-10.24 4.608-10.24 10.24v30.72h-36.352l-8.192-86.528c0-3.072 2.048-5.12 5.632-5.632zM819.2 358.4v71.68h-112.64V358.4h112.64z m-501.76 0v71.68H204.8V358.4h112.64z m513.024 461.312c0 2.56-2.56 4.608-5.12 4.608H198.144c-2.56 0-4.608-2.048-5.12-4.608L154.624 399.36H189.44v35.84c0 5.632 4.608 10.24 10.24 10.24h122.88c5.632 0 10.24-4.608 10.24-10.24v-35.84h358.4v35.84c0 5.632 4.608 10.24 10.24 10.24h122.88c5.632 0 10.24-4.608 10.24-10.24v-35.84h34.816l-38.912 420.352z" fill=""/>
</g>
</svg>
""")
}
private func exclamationSvg(size: SVGSize) -> HTMLRaw {
HTMLRaw("""
<svg xmlns="http://www.w3.org/2000/svg" width="\(size.width)" height="\(size.height)" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-6 h-6">
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"></path>
<path d="M12 9v4"></path>
<path d="M12 17h.01"></path>
</svg>
""")
}
private func windSvg(size: SVGSize) -> HTMLRaw {
return HTMLRaw("""
<svg xmlns="http://www.w3.org/2000/svg" width="\(size.width)" height="\(size.height)" viewBox="0 0 \(size.width) \(size.height)" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="w-8 h-8">
<path d="M17.7 7.7a2.5 2.5 0 1 1 1.8 4.3H2"></path>
<path d="M9.6 4.6A2 2 0 1 1 11 8H2"></path>
<path d="M12.6 19.4A2 2 0 1 0 14 16H2"></path>
</svg>
""")
}
private func calculatorSvg(size: SVGSize) -> HTMLRaw {
return HTMLRaw("""
<svg xmlns="http://www.w3.org/2000/svg" width="\(size.width)" height="\(size.height)" viewBox="0 0 \(size.width) \(size.height)" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="w-5 h-5">
<rect width="16" height="20" x="4" y="2" rx="2"></rect>
<line x1="8" x2="16" y1="6" y2="6"></line>
<line x1="16" x2="16" y1="14" y2="18"></line>
<path d="M16 10h.01"></path>
<path d="M12 10h.01"></path>
<path d="M8 10h.01"></path>
<path d="M12 14h.01"></path>
<path d="M8 14h.01"></path>
<path d="M12 18h.01"></path><path d="M8 18h.01"></path>
</svg>
""")
}
private func thermometerSvg(size: SVGSize) -> HTMLRaw {
HTMLRaw("""
<svg xmlns="http://www.w3.org/2000/svg" width="\(size.width)" height="\(size.height)" viewBox="0 0 \(size.width) \(size.height)" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="w-8 h-8">
<path d="M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z"></path>
</svg>
""")
}
private func menuSvg(size: SVGSize) -> HTMLRaw {
HTMLRaw("""
<svg class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor"
viewBox="0 0 24 24" x="0" y="0" id="menu-icon">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
""")
}
// swiftlint:enable line_length