Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Style Protocol Redux: Introducing a reimagined Style protocol for generating custom CSS classes #222

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
6 changes: 5 additions & 1 deletion Sources/Ignite/Extensions/Array-DefinitelyNotAHack.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@
//

// swiftlint:disable unused_setter_value
extension Array: HeadElement, HTML, HorizontalAligning where Element: HTML {
extension Array: HeadElement, HTML, Modifiable, HorizontalAligning where Element: HTML {
public var body: some HTML { self }

public func render(context: PublishingContext) -> String {
self.map { $0.render(context: context) }.joined()
}

@MainActor func style(_ property: Property, _ value: String) -> some HTML {
self.map { $0.style(.init(name: property, value: value)) }
}
}

extension Array: BlockHTML where Element: BlockHTML {
Expand Down
2 changes: 1 addition & 1 deletion Sources/Ignite/Framework/Content.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Foundation

/// One piece of Markdown content for this site.
@MainActor
public struct Content: Sendable {
public struct Content {
/// The main title for this content.
public var title: String

Expand Down
12 changes: 11 additions & 1 deletion Sources/Ignite/Framework/ElementTypes/HTML.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
/// You typically don't conform to `HTML` directly. Instead, use one of the built-in elements like
/// `Div`, `Paragraph`, or `Link`, or create custom components by conforming to `HTMLRootElement`.
@MainActor
public protocol HTML: Sendable {
public protocol HTML: Modifiable, Sendable {
/// A unique identifier used to track this element's state and attributes.
var id: String { get set }

Expand Down Expand Up @@ -195,6 +195,16 @@ public extension HTML {
return self
}

/// Adds inline styles to the element.
/// - Parameter values: Variable number of `AttributeValue` objects
/// - Returns: The modified `HTML` element
func style(_ values: AttributeValue...) -> Self {
var attributes = attributes
attributes.styles.formUnion(values)
AttributeStore.default.merge(attributes, intoHTML: id)
return self
}

/// Sets the `HTML` id attribute of the element.
/// - Parameter string: The ID value to set
/// - Returns: The modified `HTML` element
Expand Down
2 changes: 1 addition & 1 deletion Sources/Ignite/Framework/Environment/EnvironmentKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
/// }
/// ```
@MainActor
public protocol EnvironmentKey: Sendable {
public protocol EnvironmentKey {
/// The type of value associated with this environment key.
associatedtype Value: Sendable

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
/// }
/// ```
@MainActor
public struct EnvironmentValues: Sendable {
public struct EnvironmentValues {
/// Provides access to the Markdown pages on this site.
public var content: ContentLoader

Expand Down
106 changes: 106 additions & 0 deletions Sources/Ignite/Framework/EnvironmentConditions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
//
// EnvironmentConditions.swift
// Ignite
// https://www.github.com/twostraws/Ignite
// See LICENSE for license information.
//

/// A type that represents a combination of environment conditions for styling content.
///
/// Use `EnvironmentConditions` to define how content should be styled based on various
/// device and user preferences. For example:
///
/// ```swift
/// struct MyStyle: Style {
/// func style(content: StyledHTML, environment: EnvironmentConditions) -> StyledHTML {
/// if environment.colorScheme == .dark && environment.orientation == .portrait {
/// content.foregroundStyle(.red)
/// } else {
/// content
/// }
/// }
/// }
/// ```
public struct EnvironmentConditions: Equatable, Hashable, Sendable {
/// The user's preferred color scheme.
public var colorScheme: ColorSchemeQuery?

/// The user's preferred motion settings.
public var motion: MotionQuery?

/// The user's preferred contrast settings.
public var contrast: ContrastQuery?

/// The user's preferred transparency settings.
public var transparency: TransparencyQuery?

/// The device's current orientation.
public var orientation: OrientationQuery?

/// The web application's display mode.
public var displayMode: DisplayModeQuery?

/// The current breakpoint query.
public var breakpoint: BreakpointQuery?

/// The current theme identifier.
public var theme: String?

/// Creates a new environment conditions instance.
/// - Parameters:
/// - colorScheme: The preferred color scheme
/// - motion: The preferred motion settings
/// - contrast: The preferred contrast settings
/// - transparency: The preferred transparency settings
/// - orientation: The device orientation
/// - displayMode: The display mode
/// - breakpoint: The breakpoint query
/// - theme: The theme identifier
init(
colorScheme: ColorSchemeQuery? = nil,
motion: MotionQuery? = nil,
contrast: ContrastQuery? = nil,
transparency: TransparencyQuery? = nil,
orientation: OrientationQuery? = nil,
displayMode: DisplayModeQuery? = nil,
breakpoint: BreakpointQuery? = nil,
theme: String? = nil
) {
self.colorScheme = colorScheme
self.motion = motion
self.contrast = contrast
self.transparency = transparency
self.orientation = orientation
self.displayMode = displayMode
self.breakpoint = breakpoint
self.theme = theme
}

/// Converts the environment conditions to an array of media queries.
/// - Returns: An array of ``Query`` values representing the active conditions.
func toMediaQueries() -> [any Query] {
var queries: [any Query] = []
if let colorScheme { queries.append(.colorScheme(colorScheme)) }
if let motion { queries.append(.motion(motion)) }
if let contrast { queries.append(.contrast(contrast)) }
if let transparency { queries.append(.transparency(transparency)) }
if let orientation { queries.append(.orientation(orientation)) }
if let displayMode { queries.append(.displayMode(displayMode)) }
if let theme { queries.append(.theme(theme)) }
if let breakpoint { queries.append(.breakpoint(breakpoint)) }
return queries
}

var conditionCount: Int {
var count = 0
if colorScheme != nil { count += 1 }
if orientation != nil { count += 1 }
if transparency != nil { count += 1 }
if displayMode != nil { count += 1 }
if motion != nil { count += 1 }
if contrast != nil { count += 1 }
if breakpoint != nil { count += 1 }
if theme != nil { count += 1 }
return count
}
}
2 changes: 1 addition & 1 deletion Sources/Ignite/Framework/Layout.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
/// }
/// ```
@MainActor
public protocol Layout: Sendable {
public protocol Layout {
/// The type of HTML content this layout will generate
associatedtype Body: HTML

Expand Down
14 changes: 14 additions & 0 deletions Sources/Ignite/Framework/Modifiable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//
// Modifiable.swift
// Ignite
// https://www.github.com/twostraws/Ignite
// See LICENSE for license information.
//

/// A type that can be modified with style attributes.
@MainActor public protocol Modifiable {
/// Applies style attributes to this instance.
/// - Parameter values: A variadic list of style attributes to apply.
/// - Returns: A new instance with the style attributes applied.
@discardableResult func style(_ values: AttributeValue...) -> Self
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
/// This class provides a way to temporarily set the current page
/// while rendering HTML elements that need access to page-level information.
@MainActor
final class PageContext: Sendable {
final class PageContext {
/// The current page being rendered. Defaults to an empty page.
static var current: Page = .empty

Expand Down
2 changes: 1 addition & 1 deletion Sources/Ignite/Framework/Site.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

/// Describes one site being generated by Ignite.
@MainActor
public protocol Site: Sendable {
public protocol Site {
/// The type of your homepage. Required.
associatedtype HomePageLayout: StaticLayout

Expand Down
32 changes: 32 additions & 0 deletions Sources/Ignite/Framework/Style.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//
// Style.swift
// Ignite
// https://www.github.com/twostraws/Ignite
// See LICENSE for license information.
//

/// A protocol that defines a style that can be resolved based on environment conditions.
/// Styles are used to create reusable visual treatments that can adapt based on media queries
/// and theme settings.
///
/// Example:
/// ```swift
/// struct MyCustomStyle: Style {
/// func style(content: StyledHTML, environment: EnvironmentConditions) -> StyledHTML {
/// if environment.colorScheme == .dark {
/// content.foregroundStyle(.red)
/// } else {
/// content.foregroundStyle(.blue)
/// }
/// }
/// }
/// ```
@MainActor
public protocol Style: Hashable {
/// Resolves the style for the given HTML content and environment conditions
/// - Parameters:
/// - content: An HTML element to apply styles to
/// - environmentConditions: The current media query condition to resolve against
/// - Returns: A modified HTML element with the appropriate styles applied
func style(content: StyledHTML, environment: EnvironmentConditions) -> StyledHTML
}
38 changes: 38 additions & 0 deletions Sources/Ignite/Framework/StyleModifier.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// ForegroundStyle.swift
// Ignite
// https://www.github.com/twostraws/Ignite
// See LICENSE for license information.
//

struct StyleModifier: HTMLModifier {
/// The style to apply.
let style: any Style

/// Applies the background style to the provided HTML content.
/// - Parameter content: The HTML content to modify
/// - Returns: The modified HTML content with background styling applied
func body(content: some HTML) -> any HTML {
let className = StyleManager.default.className(for: style)
StyleManager.default.registerStyle(style)
return content.class(className)
}
}

public extension HTML {
/// Applies a custom style to the element.
/// - Parameter style: The style to apply, conforming to the `Style` protocol
/// - Returns: A modified copy of the element with the style applied
func style(_ style: any Style) -> some HTML {
modifier(StyleModifier(style: style))
}
}

public extension BlockHTML {
/// Applies a custom style to the block element.
/// - Parameter style: The style to apply, conforming to the `Style` protocol
/// - Returns: A modified copy of the block element with the style applied
func style(_ style: any Style) -> some BlockHTML {
modifier(StyleModifier(style: style))
}
}
21 changes: 21 additions & 0 deletions Sources/Ignite/Framework/StyledHTML.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
// StyledHTML.swift
// Ignite
// https://www.github.com/twostraws/Ignite
// See LICENSE for license information.
//

/// A concrete type used for style resolution that only holds attributes
@MainActor public struct StyledHTML: Modifiable {
/// A collection of styles, classes, and attributes.
var attributes = CoreAttributes()

/// Adds inline styles to the element.
/// - Parameter values: Variable number of `AttributeValue` objects
/// - Returns: The modified `HTML` element
@discardableResult public func style(_ values: AttributeValue...) -> Self {
var copy = self
copy.attributes.append(styles: values)
return copy
}
}
17 changes: 17 additions & 0 deletions Sources/Ignite/Modifiers/Background.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,20 @@ public extension BlockHTML {
modifier(BackgroundModifier(background: .gradient(gradient)))
}
}

public extension StyledHTML {
/// Applies a background color from a `Color` object.
/// - Parameter color: The specific color value to use, specified as
/// a `Color` instance.
/// - Returns: The current element with the updated background color.
func background(_ color: Color) -> Self {
self.style(.init(name: .backgroundColor, value: color.description))
}

/// Applies a gradient background
/// - Parameter gradient: The gradient to apply
/// - Returns: The modified HTML element
func background(_ gradient: Gradient) -> Self {
self.style(.init(name: .backgroundImage, value: gradient.description))
}
}
Loading