diff --git a/Sources/Slipstream/Documentation.docc/Views/TailwindCSS/Margin.md b/Sources/Slipstream/Documentation.docc/Views/TailwindCSS/Margin.md new file mode 100644 index 0000000..b87e50d --- /dev/null +++ b/Sources/Slipstream/Documentation.docc/Views/TailwindCSS/Margin.md @@ -0,0 +1,12 @@ +# Margin + +## Topics + +### Modifiers + +- ``View/margin(_:_:)`` + +### Enumerations + +- ``Edge`` +- ``MarginValue`` diff --git a/Sources/Slipstream/Documentation.docc/Views/TailwindCSS/TailwindCSSClasses.md b/Sources/Slipstream/Documentation.docc/Views/TailwindCSS/TailwindCSSClasses.md index 1e72245..986b9fb 100644 --- a/Sources/Slipstream/Documentation.docc/Views/TailwindCSS/TailwindCSSClasses.md +++ b/Sources/Slipstream/Documentation.docc/Views/TailwindCSS/TailwindCSSClasses.md @@ -10,6 +10,7 @@ Slipstream implementations of Tailwind CSS's classes. ### Spacing +- - ### Typography diff --git a/Sources/Slipstream/TailwindCSS/Edge.swift b/Sources/Slipstream/TailwindCSS/Edge.swift index ee042b9..385b0c2 100644 --- a/Sources/Slipstream/TailwindCSS/Edge.swift +++ b/Sources/Slipstream/TailwindCSS/Edge.swift @@ -21,7 +21,7 @@ public enum Edge: Int8, CaseIterable { /// /// ```swift /// let edges: Edge.Set = [.top, .left] - /// ``` + /// ``` public struct Set: OptionSet { /// The element type of the option set. /// @@ -66,4 +66,52 @@ public enum Edge: Int8, CaseIterable { /// A convenience option representing all edges (top, left, bottom, right). public static let all: Edge.Set = [.top, .left, .bottom, .right] } + + /// Maps a point length to the closest Tailwind CSS spacing class. + /// + /// - Parameter size: The size in points to be mapped. + /// - Returns: The Tailwind CSS spacing class. + static func pointToTailwindSpacingClass(ptLength: Double) -> String { + // Tailwind spacing classes and their corresponding sizes in points. + let mapping: [(name: String, ptLength: Double)] = [ + ("0", 0), // 0pt + ("0.5", 2), // 0.125rem + ("1", 4), // 0.25rem + ("1.5", 6), // 0.375rem + ("2", 8), // 0.5rem + ("2.5", 10), // 0.625rem + ("3", 12), // 0.75rem + ("3.5", 14), // 0.875rem + ("4", 16), // 1rem + ("5", 20), // 1.25rem + ("6", 24), // 1.5rem + ("7", 28), // 1.75rem + ("8", 32), // 2rem + ("9", 36), // 2.25rem + ("10", 40), // 2.5rem + ("11", 44), // 2.75rem + ("12", 48), // 3rem + ("14", 56), // 3.5rem + ("16", 64), // 4rem + ("20", 80), // 5rem + ("24", 96), // 6rem + ("28", 112), // 7rem + ("32", 128), // 8rem + ("36", 144), // 9rem + ("40", 160), // 10rem + ("44", 176), // 11rem + ("48", 192), // 12rem + ("52", 208), // 13rem + ("56", 224), // 14rem + ("60", 240), // 15rem + ("64", 256), // 16rem + ("72", 288), // 18rem + ("80", 320), // 20rem + ("96", 384) // 24rem + ] + + // Find the closest matching spacing class + let closestClass = mapping.min { abs($0.ptLength - ptLength) < abs($1.ptLength - ptLength) } + return closestClass?.name ?? "0" + } } diff --git a/Sources/Slipstream/TailwindCSS/Spacing/View+margin.swift b/Sources/Slipstream/TailwindCSS/Spacing/View+margin.swift new file mode 100644 index 0000000..6fc9fcc --- /dev/null +++ b/Sources/Slipstream/TailwindCSS/Spacing/View+margin.swift @@ -0,0 +1,105 @@ +/// A struct representing a margin value for layout purposes, with support +/// for both numerical values and the "auto" value. +@available(iOS 17.0, macOS 14.0, *) +public struct MarginValue { + /// A static instance representing the "auto" margin value. + public static let auto = MarginValue(.auto) + + fileprivate init(integerLiteral value: Int) { + self.storage = .rawValue(Double(value)) + } + + fileprivate init(floatLiteral value: Double) { + self.storage = .rawValue(value) + } + + fileprivate init(_ value: Storage) { + self.storage = value + } + + fileprivate enum Storage { + case rawValue(Double) + case auto + } + fileprivate let storage: Storage + + /// Returns the margin value as the closest Tailwind CSS spacing class. + /// + /// - Returns: The Tailwind CSS spacing class string. + fileprivate func toTailwindSpacingClass() -> String { + switch storage { + case .rawValue(let ptLength): + return Edge.pointToTailwindSpacingClass(ptLength: ptLength) + case .auto: + return "auto" + } + } +} + +extension View { + /// Set the margin for specific edges. + /// + /// - Parameters: + /// - edges: The edges to which margin should be applied. + /// - length: The size of the margin to apply, in points. If the margin is exactly between + /// two margin classes, then the smaller margin class will be used. + /// + /// - SeeAlso: Tailwind CSS' [`margin`](https://tailwindcss.com/docs/margin) documentation. + @available(iOS 17.0, macOS 14.0, *) + public func margin(_ edges: Edge.Set, _ length: MarginValue) -> some View { + let spacingClass = length.toTailwindSpacingClass() + var classes = [String]() + + if Edge.Set.all.isSubset(of: edges) { + classes = ["m-" + spacingClass] + } else { + if Edge.Set.horizontal.isSubset(of: edges) { + classes.append("mx-" + spacingClass) + } else { + if edges.contains(.left) { + classes.append("ml-" + spacingClass) + } + if edges.contains(.right) { + classes.append("mr-" + spacingClass) + } + } + if Edge.Set.vertical.isSubset(of: edges) { + classes.append("my-" + spacingClass) + } else { + if edges.contains(.top) { + classes.append("mt-" + spacingClass) + } + if edges.contains(.bottom) { + classes.append("mb-" + spacingClass) + } + } + } + return modifier(ClassModifier(add: classes.joined(separator: " "))) + } + + /// Set the margin for specific edges. + /// + /// - Parameters: + /// - edges: The edges to which margin should be applied. + /// - length: The size of the margin to apply, in points. If the margin is exactly between + /// two margin classes, then the smaller margin class will be used. + /// + /// - SeeAlso: Tailwind CSS' [`margin`](https://tailwindcss.com/docs/margin) documentation. + @available(iOS 17.0, macOS 14.0, *) + public func margin(_ edges: Edge.Set, _ length: Double) -> some View { + margin(edges, MarginValue(floatLiteral: length)) + } + + /// Set the margin for specific edges. + /// + /// - Parameters: + /// - edges: The edges to which margin should be applied. + /// - length: The size of the margin to apply, in points. If the margin is exactly between + /// two margin classes, then the smaller margin class will be used. + /// + /// - SeeAlso: Tailwind CSS' [`margin`](https://tailwindcss.com/docs/margin) documentation. + @available(iOS 17.0, macOS 14.0, *) + public func margin(_ edges: Edge.Set, _ length: Int) -> some View { + margin(edges, MarginValue(integerLiteral: length)) + } +} diff --git a/Sources/Slipstream/TailwindCSS/Spacing/View+padding.swift b/Sources/Slipstream/TailwindCSS/Spacing/View+padding.swift index aedd8b9..e076be2 100644 --- a/Sources/Slipstream/TailwindCSS/Spacing/View+padding.swift +++ b/Sources/Slipstream/TailwindCSS/Spacing/View+padding.swift @@ -9,82 +9,33 @@ extension View { /// - SeeAlso: Tailwind CSS' [`padding`](https://tailwindcss.com/docs/padding) documentation. @available(iOS 17.0, macOS 14.0, *) public func padding(_ edges: Edge.Set, _ length: Double) -> some View { - let paddingClass = closestTailwindPadding(ptLength: length) + let spacingClass = Edge.pointToTailwindSpacingClass(ptLength: length) var classes = [String]() if Edge.Set.all.isSubset(of: edges) { - classes = ["p-" + paddingClass] + classes = ["p-" + spacingClass] } else { if Edge.Set.horizontal.isSubset(of: edges) { - classes.append("px-" + paddingClass) + classes.append("px-" + spacingClass) } else { if edges.contains(.left) { - classes.append("pl-" + paddingClass) + classes.append("pl-" + spacingClass) } if edges.contains(.right) { - classes.append("pr-" + paddingClass) + classes.append("pr-" + spacingClass) } } if Edge.Set.vertical.isSubset(of: edges) { - classes.append("py-" + paddingClass) + classes.append("py-" + spacingClass) } else { if edges.contains(.top) { - classes.append("pt-" + paddingClass) + classes.append("pt-" + spacingClass) } if edges.contains(.bottom) { - classes.append("pb-" + paddingClass) + classes.append("pb-" + spacingClass) } } } - return self.modifier(ClassModifier(add: classes.joined(separator: " "))) - } - - /// Map a point size to the closest Tailwind CSS padding class. - /// - /// - Parameter ptLength: The size, in points, to be mapped. - /// - Returns: The Tailwind CSS padding class string. - @available(iOS 17.0, macOS 14.0, *) - private func closestTailwindPadding(ptLength: Double) -> String { - // Tailwind padding classes and their corresponding sizes in points - let paddingMapping: [(paddingClass: String, ptLength: Double)] = [ - ("0", 0), // 0pt - ("0.5", 2), // 0.125rem - ("1", 4), // 0.25rem - ("1.5", 6), // 0.375rem - ("2", 8), // 0.5rem - ("2.5", 10), // 0.625rem - ("3", 12), // 0.75rem - ("3.5", 14), // 0.875rem - ("4", 16), // 1rem - ("5", 20), // 1.25rem - ("6", 24), // 1.5rem - ("7", 28), // 1.75rem - ("8", 32), // 2rem - ("9", 36), // 2.25rem - ("10", 40), // 2.5rem - ("11", 44), // 2.75rem - ("12", 48), // 3rem - ("14", 56), // 3.5rem - ("16", 64), // 4rem - ("20", 80), // 5rem - ("24", 96), // 6rem - ("28", 112), // 7rem - ("32", 128), // 8rem - ("36", 144), // 9rem - ("40", 160), // 10rem - ("44", 176), // 11rem - ("48", 192), // 12rem - ("52", 208), // 13rem - ("56", 224), // 14rem - ("60", 240), // 15rem - ("64", 256), // 16rem - ("72", 288), // 18rem - ("80", 320), // 20rem - ("96", 384) // 24rem - ] - - // Find the closest matching padding size class - let closestPadding = paddingMapping.min { abs($0.ptLength - ptLength) < abs($1.ptLength - ptLength) } - return closestPadding?.paddingClass ?? "0" + return modifier(ClassModifier(add: classes.joined(separator: " "))) } } diff --git a/Sources/Slipstream/TailwindCSS/Typography/View+fontSize.swift b/Sources/Slipstream/TailwindCSS/Typography/View+fontSize.swift index 356025a..960546c 100644 --- a/Sources/Slipstream/TailwindCSS/Typography/View+fontSize.swift +++ b/Sources/Slipstream/TailwindCSS/Typography/View+fontSize.swift @@ -27,7 +27,7 @@ extension View { /// - SeeAlso: Tailwind CSS' [`font-size`](https://tailwindcss.com/docs/font-size) documentation. @available(iOS 17.0, macOS 14.0, *) public func fontSize(_ fontSize: FontSize) -> some View { - return self.modifier(ClassModifier(add: "text-" + fontSize.rawValue)) + return modifier(ClassModifier(add: "text-" + fontSize.rawValue)) } /// Set the font size to the closest equivalent Tailwind CSS font size. diff --git a/Sources/Slipstream/TailwindCSS/Typography/View+fontSmoothing.swift b/Sources/Slipstream/TailwindCSS/Typography/View+fontSmoothing.swift index f86bfb7..0e124c7 100644 --- a/Sources/Slipstream/TailwindCSS/Typography/View+fontSmoothing.swift +++ b/Sources/Slipstream/TailwindCSS/Typography/View+fontSmoothing.swift @@ -15,7 +15,7 @@ extension View { /// - SeeAlso: Tailwind CSS' [`font-smoothing`](https://tailwindcss.com/docs/font-smoothing) documentation. @available(iOS 17.0, macOS 14.0, *) public func fontSmoothing(_ fontSmoothing: FontSmoothing) -> some View { - return self.modifier(ClassModifier(add: fontSmoothing.rawValue)) + return modifier(ClassModifier(add: fontSmoothing.rawValue)) } /// Set the font smoothing for a view to antialiased. diff --git a/Sources/Slipstream/TailwindCSS/Typography/View+fontWeight.swift b/Sources/Slipstream/TailwindCSS/Typography/View+fontWeight.swift index d93459a..3786f03 100644 --- a/Sources/Slipstream/TailwindCSS/Typography/View+fontWeight.swift +++ b/Sources/Slipstream/TailwindCSS/Typography/View+fontWeight.swift @@ -22,7 +22,7 @@ extension View { /// - SeeAlso: Tailwind CSS' [`font-weight`](https://tailwindcss.com/docs/font-weight) documentation. @available(iOS 17.0, macOS 14.0, *) public func fontWeight(_ fontWeight: FontWeight) -> some View { - return self.modifier(ClassModifier(add: "font-" + fontWeight.rawValue)) + return modifier(ClassModifier(add: "font-" + fontWeight.rawValue)) } /// Set the font weight to the closest equivalent Tailwind CSS font weight. diff --git a/Sources/Slipstream/TailwindCSS/Typography/View+textAlignment.swift b/Sources/Slipstream/TailwindCSS/Typography/View+textAlignment.swift index 37fd437..2092353 100644 --- a/Sources/Slipstream/TailwindCSS/Typography/View+textAlignment.swift +++ b/Sources/Slipstream/TailwindCSS/Typography/View+textAlignment.swift @@ -32,6 +32,6 @@ extension View { /// - SeeAlso: Tailwind CSS' [`text-align`](https://tailwindcss.com/docs/text-align) documentation. @available(iOS 17.0, macOS 14.0, *) public func textAlignment(_ alignment: TextAlignment) -> some View { - return self.modifier(ClassModifier(add: "text-" + alignment.rawValue)) + return modifier(ClassModifier(add: "text-" + alignment.rawValue)) } } diff --git a/Tests/SlipstreamTests/Sites/CatalogSiteTests.swift b/Tests/SlipstreamTests/Sites/CatalogSiteTests.swift index 24f9e01..bf7e12b 100644 --- a/Tests/SlipstreamTests/Sites/CatalogSiteTests.swift +++ b/Tests/SlipstreamTests/Sites/CatalogSiteTests.swift @@ -33,7 +33,9 @@ private struct CatalogSite: View { H4("Heading 4") .antialiased() H5("Heading 5") + .margin(.horizontal, .auto) H6("Heading 6") + .margin(.vertical, 32) } .padding(.horizontal, 48) } @@ -61,8 +63,8 @@ struct CatalogSiteTests {

Heading 2

Heading 3

Heading 4

-
Heading 5
-
Heading 6
+
Heading 5
+
Heading 6
diff --git a/Tests/SlipstreamTests/TailwindCSS/MarginTests.swift b/Tests/SlipstreamTests/TailwindCSS/MarginTests.swift new file mode 100644 index 0000000..3365d50 --- /dev/null +++ b/Tests/SlipstreamTests/TailwindCSS/MarginTests.swift @@ -0,0 +1,54 @@ +import Testing +import Slipstream + +private struct MarginView: View { + let margins: Int = 32 + var body: some View { + Div { + + } + .margin(.horizontal, margins) + } +} + +struct MarginTests { + @Test func auto() throws { + try #expect(renderHTML(Div {}.margin(.all, .auto)) == #"
"#) + try #expect(renderHTML(Div {}.margin(.horizontal, .auto)) == #"
"#) + try #expect(renderHTML(Div {}.margin(.vertical, .auto)) == #"
"#) + try #expect(renderHTML(Div {}.margin([.top, .left], .auto)) == #"
"#) + + try #expect(renderHTML(Div {}.margin(.top, .auto)) == #"
"#) + try #expect(renderHTML(Div {}.margin(.left, .auto)) == #"
"#) + try #expect(renderHTML(Div {}.margin(.bottom, .auto)) == #"
"#) + try #expect(renderHTML(Div {}.margin(.right, .auto)) == #"
"#) + } + + @Test func marginEdges() throws { + try #expect(renderHTML(Div {}.margin(.all, 16)) == #"
"#) + try #expect(renderHTML(Div {}.margin(.horizontal, 8)) == #"
"#) + try #expect(renderHTML(Div {}.margin(.vertical, 12)) == #"
"#) + try #expect(renderHTML(Div {}.margin([.top, .left], 24)) == #"
"#) + + try #expect(renderHTML(Div {}.margin(.top, 0)) == #"
"#) + try #expect(renderHTML(Div {}.margin(.left, 4)) == #"
"#) + try #expect(renderHTML(Div {}.margin(.bottom, 32)) == #"
"#) + try #expect(renderHTML(Div {}.margin(.right, 64)) == #"
"#) + } + + @Test func specificMarginSizes() throws { + try #expect(renderHTML(Div {}.margin(.top, 0)) == #"
"#) + try #expect(renderHTML(Div {}.margin(.top, 0.5)) == #"
"#) + try #expect(renderHTML(Div {}.margin(.top, 1)) == #"
"#) + try #expect(renderHTML(Div {}.margin(.top, 2)) == #"
"#) + try #expect(renderHTML(Div {}.margin(.top, 3)) == #"
"#) + try #expect(renderHTML(Div {}.margin(.top, 4)) == #"
"#) + try #expect(renderHTML(Div {}.margin(.top, 32)) == #"
"#) + try #expect(renderHTML(Div {}.margin(.top, 64)) == #"
"#) + } + + @Test func repeatedMarginModifications() throws { + try #expect(renderHTML(Div {}.margin(.top, 0).margin(.right, 4)) == #"
"#) + try #expect(renderHTML(Div {}.margin(.top, 0).margin(.top, 4)) == #"
"#) + } +}