diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..fa72a9a6 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: [maclong9] diff --git a/.github/ISSUE_TEMPLATE/02_FEATURE_REQUEST.md b/.github/ISSUE_TEMPLATE/02_FEATURE_REQUEST.md index cfba3922..fe9893a6 100644 --- a/.github/ISSUE_TEMPLATE/02_FEATURE_REQUEST.md +++ b/.github/ISSUE_TEMPLATE/02_FEATURE_REQUEST.md @@ -31,6 +31,7 @@ assignees: '' - [ ] Extension to Existing Element - [ ] Performance Improvement - [ ] Documentation Enhancement +- [ ] Developer Experience Enhancement - [ ] Other (please specify) ## Example Code diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 70001c43..9d251b69 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -215,9 +215,10 @@ WebUI follows a compositional pattern for creating HTML elements. When adding a ## Adding New Style Modifiers -Style modifiers in WebUI follow an extension pattern on the `Element` class. Here's how to add a new style modifier: +Style modifiers in WebUI follow the unified style system pattern. Here's how to add a new style modifier: 1. **File Location**: Add the new style modifier to the appropriate file in `Sources/WebUI/Styles/`: + - Core style operations go in the `Core/` directory - Appearance-related modifiers go in the `Appearance/` directory - Layout-related modifiers go in the `Base/` or `Positioning/` directories - Create a new file if the modifier doesn't fit an existing category @@ -238,7 +239,33 @@ Style modifiers in WebUI follow an extension pattern on the `Element` class. Her } ``` - b. **Implement the Extension Method**: + b. **Implement the Style Operation**: + ```swift + /// Style operation for the custom style + public struct StyleModifierOperation: StyleOperation { + /// Parameters for this style operation + public struct Parameters { + public let option: StyleOption + + public init(option: StyleOption) { + self.option = option + } + } + + /// Applies the style operation and returns CSS classes + public func applyClasses(params: Parameters) -> [String] { + return ["style-\(params.option.rawValue)"] + } + + /// Shared instance + public static let shared = StyleModifierOperation() + + /// Private initializer + private init() {} + } + ``` + + c. **Implement the Element Extension Method**: ```swift extension Element { /// Applies a style modifier to the element. @@ -260,35 +287,37 @@ Style modifiers in WebUI follow an extension pattern on the `Element` class. Her option: StyleOption, on modifiers: Modifier... ) -> Element { - let baseClass = "style-\(option.rawValue)" - let newClasses = combineClasses([baseClass], withModifiers: modifiers) - - return Element( - tag: self.tag, - id: self.id, - classes: (self.classes ?? []) + newClasses, - role: self.role, - label: self.label, - data: self.data, - isSelfClosing: self.isSelfClosing, - customAttributes: self.customAttributes, - content: self.contentBuilder + let params = StyleModifierOperation.Parameters(option: option) + + return StyleModifierOperation.shared.applyToElement( + self, + params: params, + modifiers: modifiers ) } } ``` - c. **Update Responsive Support** (if applicable): + d. **Implement the Global Function for Declarative DSL**: ```swift - extension Element { - /// Responsive version for use within the .responsive block - @discardableResult - public func styleModifier(option: StyleOption) -> Element { - return self.styleModifier(option: option, on: []).proxy() - } + /// Applies the style modifier in the responsive context with Declarative syntax. + /// + /// - Parameters: + /// - option: The style option to apply. + /// - Returns: A responsive modification for the style. + public func styleModifier(option: StyleOption) -> ResponsiveModification { + let params = StyleModifierOperation.Parameters(option: option) + + return StyleModifierOperation.shared.asModification(params: params) } ``` + f. **Register in StyleRegistry** (optional but recommended): + ```swift + // In StyleRegistry.swift + public static let styleModifier = StyleModifierOperation.shared + ``` + 3. **Testing**: Add unit tests for the new style modifier in the `Tests` directory. 4. **Documentation**: Include comprehensive DocC documentation with: @@ -296,7 +325,7 @@ Style modifiers in WebUI follow an extension pattern on the `Element` class. Her - Parameter documentation for all method parameters - Usage examples showing common implementations - Visual examples if the style has a significant impact on appearance - - Examples of using the modifier within responsive blocks + - Examples of using the modifier with all three interfaces (direct, on, and Declarative) ### Adding Extensions to Existing Elements @@ -332,30 +361,62 @@ You can extend existing elements with specialized methods to improve developer e 3. **Documentation**: As with other additions, include comprehensive DocC documentation. -### Using Responsive Styling - -WebUI provides a block-based responsive API for cleaner responsive designs. - -### Basic Usage - -The `.responsive` modifier allows applying multiple style changes across different breakpoints in a single block: - -```swift -Text { "Responsive Content" } - .font(size: .sm) - .background(color: .neutral(._500)) - .responsive { - $0.md { - $0.font(size: .lg) - $0.background(color: .neutral(._700)) - $0.padding(of: 4) - } - $0.lg { - $0.font(size: .xl) - $0.background(color: .neutral(._900)) - } - } -``` +## Responsive Styling System + +WebUI provides a powerful responsive styling system with multiple syntaxes for flexibility and developer experience. + +### Responsive Styling Options + +WebUI supports two different ways to apply responsive styles: + +1. **Declarative Block Syntax** - Create responsive styles in a clean, concise way: +1. **Declaritive** (using the result builder pattern): + ```swift + Text { "Responsive Content" } + .font(size: .sm) + .responsive { + md { + font(size: .lg) + background(color: .neutral(._700)) + } + lg { + font(size: .xl) + padding(of: 6) + } + } + ``` + +2. **Direct Breakpoint Modifiers** (for single property changes): + ```swift + Text { "Responsive Content" } + .font(size: .sm, on: .xs) + .font(size: .lg, on: .md) + .font(size: .xl, on: .lg) + ``` + +### Implementing Responsive Support + +When adding new style modifiers, you must implement both interfaces to ensure full responsive support: + +1. **Element Extension** - For direct styling and breakpoint modifiers +2. **Global Function** - For Declaritive syntax with result builders + +The unified style system makes this process simple by deriving both interfaces from a single style operation. + +### Unified Style System + +WebUI uses a unified style system that defines each style operation once and derives multiple interfaces from it: + +1. **Core Style Operations** - Define the core logic for generating CSS classes +2. **Interface Adapters** - Adapt the core operations to different contexts (Element extensions and global functions) +3. **StyleRegistry** - Provides centralized access to all style operations + +This approach provides several benefits: +- Single point of definition for each style +- Consistency across different interfaces +- Reduced code duplication +- Easier maintenance and extension +- Clean, Declarative syntax for responsive styling ### File Organization Principles @@ -367,18 +428,21 @@ When organizing code in the WebUI library, follow these principles: 4. **Manageable Size**: Keep files under on the smaller side where possible 5. **Clear Dependencies**: Make dependencies between components explicit and avoid circularity -When refactoring or adding new code, consider if you should: -- Add to an existing file (for small, closely related additions) -- Create a new file in an existing directory (for new components in an established category) -- Create a new directory (for entirely new subsystems or categories) +When adding new style operations: +- Create the style operation in `Styles/Core/` +- Register it in `StyleRegistry` +- Implement the three interface adapters (Element, ResponsiveBuilder, global function) +- Add comprehensive documentation and tests + +### Style Operation File Structure -### Adding Support to Custom Style Methods +Each style operation file should follow this structure: -When creating custom style modifiers, ensure they work within responsive blocks by: +1. **Style Operation Implementation** - The core logic for generating CSS classes +2. **Element Extension** - For direct styling with the `on: modifiers` parameter +3. **Global Function** - For Declarative syntax within responsive blocks -1. Adding a version without the `on modifiers` parameter -2. Using the `.proxy()` method to maintain the proper element chain -3. Adding examples showing how to use the style in responsive contexts +This structure ensures full support across both interfaces and maintains consistency across the codebase. ## Styleguides diff --git a/README.md b/README.md index 451892b9..ca821987 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ - [Development](#development) - [Quick Reference](#quick-reference) - [Contributing](#contributing) +- [Funding](#funding) - [Support](#support) - [License](#license) - [Acknowledgements](#acknowledgements) @@ -140,6 +141,14 @@ else and are greatly appreciated. Please see our [CONTRIBUTING.md](CONTRIBUTING.md) document for detailed guidelines on how to contribute to this project. All contributors are expected to adhere to our [Code of Conduct](CODE_OF_CONDUCT.md). +## Funding + +WebUI is an independent open-source project that relies on community support. If you find it useful, please consider supporting its development: + +- [GitHub Sponsors](https://github.com/sponsors/maclong9) - Direct support for maintainers + +Your support helps ensure WebUI remains actively maintained and continuously improved. Organizations using WebUI in production are especially encouraged to contribute financially to ensure the project's long-term sustainability. + ## Support Reach out to the maintainer at one of the following places: diff --git a/Sources/WebUI/Documentation.docc/Extensions/Date.md b/Sources/WebUI/Documentation.docc/Extensions/Date.md deleted file mode 100644 index 0816b610..00000000 --- a/Sources/WebUI/Documentation.docc/Extensions/Date.md +++ /dev/null @@ -1,12 +0,0 @@ -# ``Foundation/Date`` - -## Topics - -### Date Formatting - -- ``formatAsYear()`` - -## See Also - -- ``NSCalendar`` -- ``DateFormatter`` \ No newline at end of file diff --git a/Sources/WebUI/Documentation.docc/Extensions/String.md b/Sources/WebUI/Documentation.docc/Extensions/String.md deleted file mode 100644 index b7fa9b0a..00000000 --- a/Sources/WebUI/Documentation.docc/Extensions/String.md +++ /dev/null @@ -1,21 +0,0 @@ -# ``String`` - -## Overview - -The `String` type in WebUI has been extended to provide HTML rendering capabilities and utilities for web development. - -## Topics - -### HTML Functionality - -- ``render()`` - -### String Formatting - -- ``sanitizedForCSS()`` -- ``pathFormatted()`` - -## See Also - -- ``HTML`` -- ``Element`` \ No newline at end of file diff --git a/Sources/WebUI/Documentation.docc/Resources/code-files/about-page.swift b/Sources/WebUI/Documentation.docc/Resources/code-files/about-page.swift deleted file mode 100644 index 69665b33..00000000 --- a/Sources/WebUI/Documentation.docc/Resources/code-files/about-page.swift +++ /dev/null @@ -1,73 +0,0 @@ -import WebUI - -struct About: HTML { - var document: Document { - .init( - path: "/about", - metadata: Metadata( - title: "About", - description: "Learn more about this website and its creator" - ), - content: { self } - ) - } - - func render() -> String { - Layout { - Section { - Heading(.title) { - "About This Site" - } - .font(size: .xl3, weight: .bold) - .margins(at: .bottom, 6) - - Text { - "This is the About page of my static website built with WebUI. This page demonstrates how to create multiple pages that share common layout elements but have unique content." - } - .margins(at: .bottom, 4) - - Heading(.headline) { - "About the Creator" - } - .font(size: .xl, weight: .bold) - .margins(at: .horizontal, 3) - - Text { - "I'm a Swift developer who loves building clean, fast websites. This site serves as a demonstration of using Swift for web development." - } - .margins(at: .bottom, 4) - - Heading(.headline) { - "Built with WebUI" - } - .font(size: .xl, weight: .bold) - .margins(at: .vertical) - - Text { - "WebUI is a Swift library that makes it easy to build static websites using a component-based approach. Key features include:" - } - .margins(of: 4, at: .bottom) - - List { - Item { "Type-safe HTML generation" } - Item { "Tailwind-inspired styling API" } - Item { "Component reusability" } - Item { "Simple static site generation" } - } - .padding(EdgeInsets(leading: 8)) - .margins(of: 4, at: .bottom) - - Link(to: "/") { - "Return to Home Page" - } - .margins(of: 4, at: .top) - .background(color: .blue(._500)) - .font(color: .custom("white")) - .padding(EdgeInsets(vertical: 2, horizontal: 4)) - .rounded(.md) - } - .frame(maxWidth: .fraction(2, 3)) - .margins(at: .horizontal, auto: true) - }.render() - } -} diff --git a/Sources/WebUI/Documentation.docc/Resources/code-files/application-main.swift b/Sources/WebUI/Documentation.docc/Resources/code-files/application-main.swift deleted file mode 100644 index fccb1f98..00000000 --- a/Sources/WebUI/Documentation.docc/Resources/code-files/application-main.swift +++ /dev/null @@ -1,33 +0,0 @@ -import Foundation -import WebUI - -@main -actor Application { - static let metadata = Metadata( - site: "My WebUI Site", - description: "A static website built with Swift and WebUI", - author: "Swift Developer", - keywords: [ - "swift", "web development", "static site", "webui", - ], - type: .website - ) - - static let routes = [ - HomePage().document, - AboutPage().document, - ] - - static func main() async { - do { - try Website( - metadata: metadata, - theme: theme, - stylesheets: stylesheets, - routes: routes, - ).build() - } catch { - print("Failed to build application: \(error)") - } - } -} diff --git a/Sources/WebUI/Documentation.docc/Resources/code-files/edge-insets-example.swift b/Sources/WebUI/Documentation.docc/Resources/code-files/edge-insets-example.swift deleted file mode 100644 index 8d2777d5..00000000 --- a/Sources/WebUI/Documentation.docc/Resources/code-files/edge-insets-example.swift +++ /dev/null @@ -1,77 +0,0 @@ -import WebUI - -// Example component showing various uses of EdgeInsets -struct CardComponent: HTML { - let title: String - let content: String - let imageSrc: String? - - init(title: String, content: String, imageSrc: String? = nil) { - self.title = title - self.content = content - self.imageSrc = imageSrc - } - - func render() -> String { - Stack(classes: ["card"]) { - // Card header with different top/bottom padding - Stack(classes: ["card-header"]) { - Heading(.subtitle) { title } - .font(weight: .semibold) - } - .background(color: .gray(._100)) - .padding(EdgeInsets(top: 4, leading: 6, bottom: 4, trailing: 6)) - .border(EdgeInsets(top: 0, leading: 0, bottom: 1, trailing: 0), style: .solid, color: .gray(._200)) - - // Card content with uniform padding - Stack(classes: ["card-body"]) { - if let src = imageSrc { - Image(src: src, alt: "Card image") - .margins(of: 4, at: .bottom) - .frame(width: .full) - } - - Text { content } - .font(size: .sm) - } - .padding(EdgeInsets(all: 6)) - - // Card footer with different horizontal/vertical padding - Stack(classes: ["card-footer"]) { - Link(to: "#") { "Learn More" } - .background(color: .blue(._500)) - .font(color: .white) - .padding(EdgeInsets(vertical: 2, horizontal: 4)) - .rounded(.md) - } - .padding(EdgeInsets(vertical: 4, horizontal: 6)) - .background(color: .gray(._50)) - .border(EdgeInsets(top: 1, leading: 0, bottom: 0, trailing: 0), style: .solid, color: .gray(._200)) - } - .background(color: .white) - .rounded(.lg) - .shadow(of: .md) - // Responsive styling with EdgeInsets - .responsive { - $0.md { - // Uniform margins on medium screens - $0.margins(EdgeInsets(all: 4)) - } - $0.lg { - // Custom margins on large screens - centered with auto horizontal margins - $0.margins(EdgeInsets(vertical: 6, horizontal: 0), auto: true) - // Override width on large screens - $0.frame(maxWidth: 600) - } - } - .render() - } -} - -// Usage example -let card = CardComponent( - title: "Using EdgeInsets for Precise Spacing", - content: - "EdgeInsets provide a convenient way to specify different spacing values for each edge of an element with a single method call. They can be used with padding, margins, borders, and positions.", - imageSrc: "/images/spacing-diagram.png" -) diff --git a/Sources/WebUI/Documentation.docc/Resources/code-files/home-page.swift b/Sources/WebUI/Documentation.docc/Resources/code-files/home-page.swift deleted file mode 100644 index b3db23fc..00000000 --- a/Sources/WebUI/Documentation.docc/Resources/code-files/home-page.swift +++ /dev/null @@ -1,43 +0,0 @@ -import WebUI - -struct Home: HTML { - var document: Document { - .init( - path: "/", - metadata: Metadata( - title: "Home", - description: "Welcome to my personal website built with WebUI" - ), - content: { self } - ) - } - - func render() -> String { - Layout { - Section { - Heading(.title) { - "Welcome to My Website" - } - .font(size: .xl3, weight: .bold) - .margins(at: .bottom) - - Text { - "This is a simple static website built with WebUI and Swift. WebUI makes it easy to create clean, semantic HTML without writing any HTML directly." - } - .margins(at: .bottom) - - Text { - "This homepage is one of two pages in our site. You can navigate to the About page using the link in the header." - } - .margins(at: .bottom) - - Text { - "Learn more about WebUI by exploring this site!" - } - .font(weight: .semibold) - } - .frame(maxWidth: .fraction(2, 3)) - .margins(at: .horizontal, auto: true) - }.render() - } -} diff --git a/Sources/WebUI/Documentation.docc/Resources/code-files/layout-component.swift b/Sources/WebUI/Documentation.docc/Resources/code-files/layout-component.swift deleted file mode 100644 index 85b2ec49..00000000 --- a/Sources/WebUI/Documentation.docc/Resources/code-files/layout-component.swift +++ /dev/null @@ -1,53 +0,0 @@ -import Foundation -import WebUI - -struct Layout: HTML { - let children: Children - - init(@HTMLBuilder children: @escaping HTMLContentBuilder) { - self.children = Children(content: children) - } - - public func render() -> String { - Stack { - Header { - Link(to: "/") { Application.metadata.site ?? "" }.font(size: .xl2) - Navigation { - for path in Application.routes { - Link(to: path.path ?? "") { path.metadata.title ?? "" } - .nav() - .font(size: .sm) - } - }.flex(align: .center).spacing(of: 2, along: .x) - } - .flex(justify: .between, align: .center) - .frame(width: .screen) - .margins(at: .bottom) - .padding(of: 6, at: .all) - - Main { - children.render() - } - .flex(grow: .one) - .font(wrapping: .pretty) - - Footer { - Text { - "© \(Date().formattedYear()) " - Link(to: "/") { Application.metadata.site ?? "" }.font( - weight: .normal - ) - }.font( - size: .sm, - color: .neutral(._700, opacity: 0.7), - ) - } - .flex(justify: .center, align: .center) - .padding(EdgeInsets(all: 4)) - } - .font(family: Application.theme.fonts["body"] ?? "system-ui") - .frame(minHeight: .screen) - .flex(direction: .column) - .render() - } -} diff --git a/Sources/WebUI/Documentation.docc/Resources/code-files/package-swift.swift b/Sources/WebUI/Documentation.docc/Resources/code-files/package-swift.swift deleted file mode 100644 index 30b3335d..00000000 --- a/Sources/WebUI/Documentation.docc/Resources/code-files/package-swift.swift +++ /dev/null @@ -1,21 +0,0 @@ -// swift-tools-version: 6.1 - -import PackageDescription - -let package = Package( - name: "example", - platforms: [.macOS(.v15)], - dependencies: [ - .package(url: "https://github.com/maclong9/web-ui.git", from: "1.0.0") - ], - targets: [ - .executableTarget( - name: "Application", - dependencies: [ - .product(name: "WebUI", package: "web-ui") - ], - path: "Sources", - resources: [.process("Public")] - ) - ] -) diff --git a/Sources/WebUI/Documentation.docc/Resources/code-files/terminal-build.sh b/Sources/WebUI/Documentation.docc/Resources/code-files/terminal-build.sh deleted file mode 100644 index 68d89996..00000000 --- a/Sources/WebUI/Documentation.docc/Resources/code-files/terminal-build.sh +++ /dev/null @@ -1,11 +0,0 @@ -λ swift run -[1/1] Planning build -Building for debugging... -[16/16] Applying Application -Build of product 'Application' complete! (0.98s) -2025-05-14T13:46:47+0100 notice com.webui.setup : [WebUI] Logging system initialized with log level: info -2025-05-14T13:46:47+0100 info com.webui.application : [WebUI] Initialized 'My WebUI Site' with 2 routes, theme: none -2025-05-14T13:46:47+0100 info com.webui.application : [WebUI] Starting build to '/Users/user/Developer/MyStaticSite/.output' with assets from 'Sources/Public' -2025-05-14T13:46:47+0100 info com.webui.application : [WebUI] Building 2 routes -2025-05-14T13:46:47+0100 info com.webui.application : [WebUI] Copying assets from '/Users/user/Developer/MyStaticSite/Sources/Public' to '/Users/user/Developer/MyStaticSite/.output/public' -2025-05-14T13:46:47+0100 info com.webui.application : [WebUI] Build completed successfully with 2 routes diff --git a/Sources/WebUI/Documentation.docc/Resources/code-files/terminal-init.sh b/Sources/WebUI/Documentation.docc/Resources/code-files/terminal-init.sh deleted file mode 100644 index 37d2771f..00000000 --- a/Sources/WebUI/Documentation.docc/Resources/code-files/terminal-init.sh +++ /dev/null @@ -1,3 +0,0 @@ -mkdir my-static-site -cd my-static-site -swift package init --type executable \ No newline at end of file diff --git a/Sources/WebUI/Documentation.docc/Resources/code-files/terminal-sources.sh b/Sources/WebUI/Documentation.docc/Resources/code-files/terminal-sources.sh deleted file mode 100644 index 766708ba..00000000 --- a/Sources/WebUI/Documentation.docc/Resources/code-files/terminal-sources.sh +++ /dev/null @@ -1,5 +0,0 @@ -mkdir -p Sources/Components Sources/Public -touch Sources/Pages/Components/Layout.swift -touch Sources/Pages/Home.swift -touch Sources/Pages/About.swift -touch Sources/Application.swift diff --git a/Sources/WebUI/Documentation.docc/Resources/code-files/terminal-structure.txt b/Sources/WebUI/Documentation.docc/Resources/code-files/terminal-structure.txt deleted file mode 100644 index 76e7dc58..00000000 --- a/Sources/WebUI/Documentation.docc/Resources/code-files/terminal-structure.txt +++ /dev/null @@ -1,9 +0,0 @@ -Sources/ - WebUI/ - Components/ - Layout.swift - Pages/ - Home.swift - About.swift - Public/ - Application.swift diff --git a/Sources/WebUI/Documentation.docc/Resources/images/final-output.png b/Sources/WebUI/Documentation.docc/Resources/images/final-output.png deleted file mode 100644 index da3918dd..00000000 Binary files a/Sources/WebUI/Documentation.docc/Resources/images/final-output.png and /dev/null differ diff --git a/Sources/WebUI/Documentation.docc/WebUI.md b/Sources/WebUI/Documentation.docc/WebUI.md index 871536e9..56ac94a0 100644 --- a/Sources/WebUI/Documentation.docc/WebUI.md +++ b/Sources/WebUI/Documentation.docc/WebUI.md @@ -22,6 +22,7 @@ With WebUI, you can: - - +- ### Tutorials @@ -61,13 +62,12 @@ With WebUI, you can: - ``BorderStyle`` - ``Modifier`` - ``Edge`` -- ``EdgeInsets`` - ``ResponsiveBuilder`` ### Responsive Design - -- ``Element/responsive(_:)`` +- ``Element/on(_:)`` - ``ResponsiveBuilder`` ### Tutorials diff --git a/Sources/WebUI/Documentation.docc/creating-a-static-site.tutorial b/Sources/WebUI/Documentation.docc/creating-a-static-site.tutorial deleted file mode 100644 index f2c946af..00000000 --- a/Sources/WebUI/Documentation.docc/creating-a-static-site.tutorial +++ /dev/null @@ -1,169 +0,0 @@ -@Tutorial(time: 30) { - @Intro(title: "Creating a Static Site") { - Learn how to build a simple static website with a home page and an about page using the WebUI library. - - This tutorial will walk you through setting up a WebUI project, creating a reusable layout component, building pages with different content, adding navigation between them, and generating the final static site. - } - - @Section(title: "Set Up Your Project") { - Create a new Swift project and add the WebUI dependency. - - @Steps { - @Step { - First, create a new directory for your project and initialize a Swift package. - - @Code(name: "Terminal", file: terminal-init.sh) - } - - @Step { - Update your Package.swift file to add the WebUI dependency. - - @Code(name: "Package.swift", file: package-swift.swift) - } - - @Step { - Create a Sources directory structure for your project. - - > This structure is an example of how you can organize your project files. - - @Code(name: "Terminal", file: terminal-sources.sh) - } - - @Step { - The directory structure will look like this: - - @Code(name: "Terminal", file: terminal-structure.txt) - } - } - } - - @Section(title: "Create a Layout Component") { - Build a reusable layout component that will provide consistent structure across all pages. - - @Steps { - @Step { - Let's create a Layout component, in `Sources/Components/Layout.swift` that will serve as the foundation for our website. Our Layout will create a struct conforming to the `HTML` protocol and accept children via an HTMLBuilder closure. It will provide a consistent header with navigation links, a main content area for page-specific content, and a footer with copyright information. - - This component will be reused across all pages to maintain a consistent look and feel. - - @Code(name: "Layout.swift", file: layout-component.swift) - } - } - } - - @Section(title: "Create the Home Page") { - Build the main landing page for your static site with a welcoming message and navigation. - - @Steps { - @Step { - Create a new Swift file called Home.swift in your Sources directory. - - In this file, we'll create a `Home` struct that conforms to the `HTML` protocol with a `document` property that creates a Document with metadata and content. We'll implement a `render()` method that uses our Layout component to wrap the page-specific content, defining a Section with a heading and paragraphs. The Layout component handles the header, navigation, and footer automatically, so we only need to focus on the main content. - - @Code(name: "Home.swift", file: home-page.swift) - } - } - } - - @Section(title: "Create the About Page") { - Add a second page with information about your site or project. - - @Steps { - @Step { - Create a new Swift file called About.swift in your Sources directory. - - The About page also uses the same Layout component but customizes it with different page metadata (title and description). It contains unique content about your site including multiple sections, feature descriptions, and a styled link button to return to the home page. By using the Layout component, we ensure consistency across pages. - - @Code(name: "About.swift", file: about-page.swift) - } - } - } - - @Section(title: "Build the Website") { - Create the main file that defines your website and builds the static HTML files. - - @Steps { - @Step { - Create an Application.swift file to define your website and build it. - - This main file imports the WebUI library and creates shared metadata for all pages. It defines a theme with fonts and colors, sets up the Application configuration, and configures the website with both pages as routes. Finally, it builds the static HTML files to the specified output directory. - - @Code(name: "Application.swift", file: application-main.swift) - } - - @Step { - Build and run your project to generate the static site. - - When the build completes, check your output directory to find the generated HTML files. You'll see index.html (Home page), about/index.html (About page), and a public directory with any assets. You can now open these files in a browser or deploy them to a web server. - - @Code(name: "Terminal", file: terminal-build.sh) - } - - @Step { - Preview the generated site locally. - - After building the site, you can preview it locally by opening the index.html file in a web browser. Alternatively, you can use a simple HTTP server to serve the files from the output directory. - - @Image(source: final-output.png, alt: "Previewing the site locally") - } - } - } - - @Assessments { - @MultipleChoice { - What benefit does the Layout component provide when building a multi-page site? - - @Choice(isCorrect: false) { - It automatically adds animations to page transitions - - @Justification { - The Layout component doesn't handle page transitions or animations. It provides a consistent structure across multiple pages. - } - } - - @Choice(isCorrect: true) { - It ensures header, navigation, and footer are consistent across all pages - - @Justification { - Correct! The Layout component encapsulates common elements like the header, navigation, and footer, ensuring they're consistent across all pages while allowing for different content. It also maintains consistent spacing using EdgeInsets across all pages. - } - } - - @Choice(isCorrect: false) { - It reduces the file size of the generated HTML - - @Justification { - While the Layout component helps with code organization and reuse, it doesn't directly impact the size of the generated HTML files. Its primary purpose is to maintain consistent structure and styling. - } - } - } - - @MultipleChoice { - How do you create a collection of pages to build as a website? - - @Choice(isCorrect: true) { - Use the `Website` struct with an array of `Document` instances - - @Justification { - Correct! The `Website` struct takes an array of `Document` instances in its `routes` parameter to define all the pages of your site. In our implementation, we configure the Application with these routes, which are then used by both the Website constructor and the Layout component. - } - } - - @Choice(isCorrect: false) { - Use the `build()` method on each `Document` - - @Justification { - Individual `Document` instances don't have a `build()` method. You need to use the `Website` struct to collect and build multiple pages. - } - } - - @Choice(isCorrect: false) { - Create a `StaticSite` with page URLs - - @Justification { - WebUI doesn't have a `StaticSite` type. You should use the `Website` struct to define your static site. - } - } - } - } -} diff --git a/Sources/WebUI/Documentation.docc/getting-started.md b/Sources/WebUI/Documentation.docc/getting-started.md index 6bb63138..59400079 100644 --- a/Sources/WebUI/Documentation.docc/getting-started.md +++ b/Sources/WebUI/Documentation.docc/getting-started.md @@ -110,25 +110,11 @@ Button(type: .submit) { "Submit" } .font(weight: .semibold) ``` -You can also use EdgeInsets for precise control over spacing on different sides: -```swift Button(type: .submit) { "Submit" } .background(color: .blue(.500)) - .font(color: .white) - // Apply different padding to each side - .padding(EdgeInsets(top: 2, leading: 6, bottom: 2, trailing: 6)) - .rounded(.md) - .font(weight: .semibold) -// Different margins for vertical and horizontal sides -Stack(classes: ["card"]) - .margins(EdgeInsets(vertical: 4, horizontal: 6)) -// Same border width on all sides -Image(src: "profile.jpg", alt: "Profile photo") - .border(EdgeInsets(all: 2), style: .solid, color: .gray(._300)) -``` ## Next Steps diff --git a/Sources/WebUI/Documentation.docc/responsive-styling.md b/Sources/WebUI/Documentation.docc/responsive-styling.md deleted file mode 100644 index 77f9aa19..00000000 --- a/Sources/WebUI/Documentation.docc/responsive-styling.md +++ /dev/null @@ -1,213 +0,0 @@ -# Responsive Styling - -Design responsive layouts and components that adapt to different screen sizes with WebUI's powerful responsive styling features. - -## Overview - -WebUI offers two approaches to responsive styling, each with its own strengths and use cases: - -1. **Modifier-based approach**: Apply styles conditionally using the `on` parameter with breakpoint modifiers like `.md` or `.lg` -2. **Block-based approach**: Define responsive styles for multiple breakpoints in a single, declarative block - -## Breakpoint Modifiers - -WebUI includes standard breakpoint modifiers that follow Tailwind CSS conventions: - -| Modifier | Screen Width | Description | -|----------|-------------|-------------| -| `.xs` | 480px+ | Extra small devices (larger phones) | -| `.sm` | 640px+ | Small devices (tablets, large phones) | -| `.md` | 768px+ | Medium devices (tablets) | -| `.lg` | 1024px+ | Large devices (desktops) | -| `.xl` | 1280px+ | Extra large devices (large desktops) | -| `.xl2` | 1536px+ | 2x Extra large devices (very large desktops) | - -## Modifier-Based Approach - -The traditional approach applies responsive styles by using the `on` parameter with breakpoint modifiers: - -```swift -Text { "Responsive Text" } - .font(size: .sm) - .font(size: .base, on: .md) - .font(size: .lg, on: .lg) - .margins(of: 2) - .margins(of: 4, on: .md) -``` - -This approach works well for: -- Simple responsive adjustments -- Single-property changes across breakpoints -- Maintaining backward compatibility - -## Block-Based Approach - -The new block-based approach allows defining responsive styles for multiple breakpoints and properties in a single, declarative block: - -```swift -Text { "Responsive Text" } - .font(size: .sm) - .responsive { - $0.md { - $0.font(size: .base) - $0.margins(of: 4) - } - $0.lg { - $0.font(size: .lg) - $0.margins(of: 6) - } - } -``` - -This approach is ideal for: -- Complex responsive layouts with multiple property changes -- Improved code organization and readability -- Clearer representation of breakpoint-specific styling - -## Complex Examples - -### Responsive Card Component - -```swift -Stack(classes: ["card"]) - // Using EdgeInsets for different padding on each side - .padding(EdgeInsets(top: 4, leading: 6, bottom: 4, trailing: 6)) - .background(color: .white) - .shadow(of: .sm) - .rounded(.md) - .responsive { - $0.md { - // Uniform padding using EdgeInsets - $0.padding(EdgeInsets(all: 6)) - $0.shadow(of: .md) - } - $0.lg { - // Different vertical/horizontal padding with EdgeInsets - $0.padding(EdgeInsets(vertical: 8, horizontal: 12)) - $0.flex(direction: .row) - } - } -``` - -### Responsive Navigation - -```swift -Navigation { - // Mobile navigation (hamburger menu) - Stack(classes: ["hamburger-menu"]) - .responsive { - $0.lg { - $0.hidden() // Hide on large screens - } - } - - // Desktop navigation links - Stack(classes: ["desktop-menu"]) - .hidden() // Hidden by default on mobile - .responsive { - $0.lg { - $0.hidden(false) // Show on large screens - $0.flex(direction: .row, justify: .between) - } - } -} -``` - -### Responsive Typography System - -```swift -// Page title -Heading(.largeTitle) { "Welcome to WebUI" } - .font(size: .xl3, weight: .bold) - .responsive { - $0.md { - $0.font(size: .xl4) - } - $0.lg { - $0.font(size: .xl5) - } - } - -// Section heading -Heading(.title) { "Key Features" } - .font(size: .xl, weight: .semibold) - // Add margins with EdgeInsets - .margins(EdgeInsets(top: 4, leading: 0, bottom: 2, trailing: 0)) - .responsive { - $0.md { - $0.font(size: .xl2) - // Different margins at medium breakpoint - $0.margins(EdgeInsets(top: 6, leading: 0, bottom: 3, trailing: 0)) - } - } -``` - -## Combining Approaches - -You can combine both approaches when needed, though the block-based approach is generally recommended for clarity: - -```swift -Button { "Sign Up" } - .background(color: .blue(._500)) - // Using EdgeInsets for different padding values - .padding(EdgeInsets(vertical: 2, horizontal: 4)) - .font(color: .white, weight: .semibold) - // One-off responsive adjustment - .rounded(.full, on: .lg) - // Block of related responsive adjustments - .responsive { - $0.md { - // Combining EdgeInsets with responsive design - $0.padding(EdgeInsets(vertical: 3, horizontal: 6)) - } - $0.lg { - $0.background(color: .blue(._600)) - // Advanced EdgeInsets with different values for each edge - $0.padding(EdgeInsets(top: 3, leading: 8, bottom: 3, trailing: 8)) - } - } -``` - -## Using EdgeInsets for Precise Spacing - -EdgeInsets allows you to define different spacing values for each edge (top, leading, bottom, trailing) in a single method call: - -```swift -// Different values for each edge -Element(tag: "div") - .padding(EdgeInsets(top: 4, leading: 6, bottom: 8, trailing: 6)) - -// Same value for all edges -Button() { "Submit" } - .margins(EdgeInsets(all: 4)) - -// Different vertical and horizontal values -Stack() - .padding(EdgeInsets(vertical: 2, horizontal: 4)) -``` - -EdgeInsets can be used with: -- `padding()`: Apply padding to elements -- `margins()`: Set margins around elements -- `border()`: Define border widths for each edge -- `position()`: Set position offsets for each edge - -## Best Practices - -1. **Group Related Styles**: Use the block-based approach to group styles that change together at specific breakpoints - -2. **Consider Mobile-First**: Start with the default mobile styles and progressively enhance for larger screens - -3. **Use Meaningful Breakpoints**: Choose breakpoints based on content needs rather than specific devices - -4. **Avoid Breakpoint Proliferation**: Limit the number of different breakpoints to maintain consistency - -5. **Use EdgeInsets for Precision**: When elements need different spacing values on each side, use EdgeInsets instead of multiple edge-specific calls - -6. **Test Across Devices**: Always test responsive designs on actual devices or using browser dev tools - -## See Also - -- -- ``Element`` -- ``Modifier`` \ No newline at end of file diff --git a/Sources/WebUI/Documentation.docc/styles.md b/Sources/WebUI/Documentation.docc/styles.md new file mode 100644 index 00000000..bce25ead --- /dev/null +++ b/Sources/WebUI/Documentation.docc/styles.md @@ -0,0 +1,107 @@ +# WebUI Style System + +## Overview + +The WebUI Style System provides a unified approach to defining and applying styles across different contexts in the framework. It eliminates code duplication by defining each style operation once and reusing it in multiple places. + +## Core Components + +### StyleOperation Protocol + +The `StyleOperation` protocol is the foundation of the system, defining a common interface for all style operations: + +```swift +public protocol StyleOperation { + associatedtype Parameters + func applyClasses(params: Parameters) -> [String] +} +``` + +Each style operation implements this protocol with its specific parameters and class generation logic. + +### Style Registry + +The `StyleRegistry` provides a central access point for all style operations: + +```swift +StyleRegistry.border // Access the border style operation +StyleRegistry.margins // Access the margins style operation +StyleRegistry.padding // Access the padding style operation +``` + +## Using Style Operations + +Style operations are automatically available in two contexts: + +1. **Element Extensions**: Apply styles directly to elements + ```swift + element.border(of: 1, at: .top, color: .blue(._500)) + ``` + +2. **Declaritive DSL**: Use in result builder context with a clean, declarative syntax + ```swift + element.responsive { + sm { + border(of: 1, at: .top) + } + } + ``` + +## Implementing a New Style Operation + +To add a new style operation: + +1. Create a new file in `Styles/Core` named `[Style]StyleOperation.swift` +2. Implement the `StyleOperation` protocol +3. Add the operation to `StyleRegistry` +4. Create the two interface variants (Element extension and global function) + +### Example Template + +```swift +import Foundation + +public struct NewStyleOperation: StyleOperation { + // Parameters struct + public struct Parameters { + // Define parameters + + public init(/* parameters */) { + // Initialize parameters + } + } + + // Implementation + public func applyClasses(params: Parameters) -> [String] { + // Generate and return CSS classes + } + + // Shared instance + public static let shared = NewStyleOperation() + + // Private initializer + private init() {} +} + +// Element extension +extension Element { + public func newStyle(/* parameters */) -> Element { + // Use the shared operation + } +} + + + +// Global function for Declaritive DSL +public func newStyle(/* parameters */) -> ResponsiveModification { + // Use the shared operation +} +``` + +## Benefits + +1. **Single Point of Definition**: Each style property defined once +2. **Maintainability**: Changes need to be made in only one place +3. **Consistency**: Guarantees consistency between different interfaces +4. **Extensibility**: Makes adding new style properties simpler +5. **Reduced Code Size**: Significantly less code to maintain diff --git a/Sources/WebUI/Styles/Appearance/BackgroundStyleOperation.swift b/Sources/WebUI/Styles/Appearance/BackgroundStyleOperation.swift new file mode 100644 index 00000000..d6717822 --- /dev/null +++ b/Sources/WebUI/Styles/Appearance/BackgroundStyleOperation.swift @@ -0,0 +1,107 @@ +import Foundation + +/// Style operation for background styling +/// +/// Provides a unified implementation for background styling that can be used across +/// Element methods and the Declarative DSL functions. +public struct BackgroundStyleOperation: StyleOperation, @unchecked Sendable { + /// Parameters for background styling + public struct Parameters { + /// The background color + public let color: Color + + /// Creates parameters for background styling + /// + /// - Parameters: + /// - color: The background color + public init(color: Color) { + self.color = color + } + + /// Creates parameters from a StyleParameters container + /// + /// - Parameter params: The style parameters container + /// - Returns: BackgroundStyleOperation.Parameters + public static func from(_ params: StyleParameters) -> Parameters { + Parameters( + color: params.get("color")! + ) + } + } + + /// Applies the background style and returns the appropriate CSS classes + /// + /// - Parameter params: The parameters for background styling + /// - Returns: An array of CSS class names to be applied to elements + public func applyClasses(params: Parameters) -> [String] { + ["bg-\(params.color.rawValue)"] + } + + /// Shared instance for use across the framework + public static let shared = BackgroundStyleOperation() + + /// Private initializer to enforce singleton usage + private init() {} +} + +// Extension for Element to provide background styling +extension Element { + /// Applies background color to the element. + /// + /// Adds a background color class based on the provided color and optional modifiers. + /// This method applies Tailwind CSS background color classes to the element. + /// + /// - Parameters: + /// - color: Sets the background color from the color palette or a custom value. + /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. + /// - Returns: A new element with updated background color classes. + /// + /// ## Example + /// ```swift + /// // Simple background color + /// Button() { "Submit" } + /// .background(color: .green(._500)) + /// + /// // Background color with modifiers + /// Button() { "Hover me" } + /// .background(color: .white, on: .dark) + /// .background(color: .blue(._500), on: .hover) + /// ``` + public func background( + color: Color, + on modifiers: Modifier... + ) -> Element { + let params = BackgroundStyleOperation.Parameters(color: color) + + return BackgroundStyleOperation.shared.applyToElement( + self, + params: params, + modifiers: modifiers + ) + } +} + +// Extension for ResponsiveBuilder to provide background styling +extension ResponsiveBuilder { + /// Applies background color in a responsive context. + /// + /// - Parameter color: The background color. + /// - Returns: The builder for method chaining. + @discardableResult + public func background(color: Color) -> ResponsiveBuilder { + let params = BackgroundStyleOperation.Parameters(color: color) + + return BackgroundStyleOperation.shared.applyToBuilder(self, params: params) + } +} + +// Global function for Declarative DSL +/// Applies background color styling in the responsive context. +/// +/// - Parameter color: The background color. +/// - Returns: A responsive modification for background color. +public func background(color: Color) -> ResponsiveModification { + let params = BackgroundStyleOperation.Parameters(color: color) + + return BackgroundStyleOperation.shared.asModification(params: params) +} diff --git a/Sources/WebUI/Styles/Appearance/Border.swift b/Sources/WebUI/Styles/Appearance/Border.swift deleted file mode 100644 index d7c3c8da..00000000 --- a/Sources/WebUI/Styles/Appearance/Border.swift +++ /dev/null @@ -1,335 +0,0 @@ -/// Defines sides for applying border radius. -/// -/// Represents individual corners or groups of corners for styling border radius. -/// -/// ## Example -/// ```swift -/// Button() { "Sign Up" } -/// .rounded(.lg, .top) -/// ``` -public enum RadiusSide: String { - /// Applies radius to all corners - case all = "" - /// Applies radius to the top side (top-left and top-right) - case top = "t" - /// Applies radius to the right side (top-right and bottom-right) - case right = "r" - /// Applies radius to the bottom side (bottom-left and bottom-right) - case bottom = "b" - /// Applies radius to the left side (top-left and bottom-left) - case left = "l" - /// Applies radius to the top-left corner - case topLeft = "tl" - /// Applies radius to the top-right corner - case topRight = "tr" - /// Applies radius to the bottom-left corner - case bottomLeft = "bl" - /// Applies radius to the bottom-right corner - case bottomRight = "br" -} - -/// Specifies sizes for border radius. -/// -/// Defines a range of radius values from none to full circular. -/// -/// ## Example -/// ```swift -/// Stack(classes: ["card"]) -/// .rounded(.xl) -/// ``` -public enum RadiusSize: String { - /// No border radius (0) - case none = "none" - /// Extra small radius (0.125rem) - case xs = "xs" - /// Small radius (0.25rem) - case sm = "sm" - /// Medium radius (0.375rem) - case md = "md" - /// Large radius (0.5rem) - case lg = "lg" - /// Extra large radius (0.75rem) - case xl = "xl" - /// 2x large radius (1rem) - case xl2 = "2xl" - /// 3x large radius (1.5rem) - case xl3 = "3xl" - /// Full radius (9999px, circular) - case full = "full" -} - -/// Defines styles for borders and outlines. -/// -/// Provides options for solid, dashed, and other border appearances. -/// -/// ## Example -/// ```swift -/// Stack() -/// .border(width: 1, style: .dashed, color: .gray(._300)) -/// ``` -public enum BorderStyle: String { - /// Solid line border - case solid = "solid" - /// Dashed line border - case dashed = "dashed" - /// Dotted line border - case dotted = "dotted" - /// Double line border - case double = "double" - /// Hidden border (none) - case hidden = "hidden" - /// No border (none) - case none = "none" - /// Divider style for child elements - case divide = "divide" -} - -/// Specifies sizes for box shadows. -/// -/// Defines shadow sizes from none to extra-large. -/// -/// ## Example -/// ```swift -/// Stack(classes: ["card"]) -/// .shadow(size: .lg, color: .gray(._300, opacity: 0.5)) -/// ``` -public enum ShadowSize: String { - /// No shadow - case none = "none" - /// Extra small shadow (2xs) - case xs2 = "2xs" - /// Extra small shadow (xs) - case xs = "xs" - /// Small shadow (sm) - case sm = "sm" - /// Medium shadow (default) - case md = "md" - /// Large shadow (lg) - case lg = "lg" - /// Extra large shadow (xl) - case xl = "xl" - /// 2x large shadow (2xl) - case xl2 = "2xl" -} - -extension Element { - /// Applies border styling to the element with specified attributes. - /// - /// Adds borders with custom width, style, and color to specified edges of an element. - /// - /// ## Example - /// ```swift - /// Stack() - /// .border(width: 2, at: .bottom, color: .blue(._500)) - /// .border(width: 1, at: .horizontal, color: .gray(._200), on: .hover) - /// ``` - public func border( - of width: Int? = nil, - at edges: Edge..., - style: BorderStyle? = nil, - color: Color? = nil, - on modifiers: Modifier... - ) -> Element { - let effectiveEdges = edges.isEmpty ? [Edge.all] : edges - var baseClasses: [String] = [] - - // Handle width - if let width = width { - if style == .divide { - for edge in effectiveEdges { - let edgePrefix = - edge == .horizontal ? "x" : edge == .vertical ? "y" : "" - if !edgePrefix.isEmpty { - baseClasses.append("divide-\(edgePrefix)-\(width)") - } - } - } else { - baseClasses.append( - contentsOf: effectiveEdges.map { edge in - let edgePrefix = edge.rawValue.isEmpty ? "" : "-\(edge.rawValue)" - return "border\(edgePrefix)\(width != 0 ? "-\(width)" : "")" - } - ) - } - } - - // Add edge-specific border classes when color is specified and width is nil - if color != nil && width == nil { - baseClasses.append( - contentsOf: effectiveEdges.map { edge in - let edgePrefix = edge.rawValue.isEmpty ? "" : "-\(edge.rawValue)" - return "border\(edgePrefix)" - } - ) - } - - // Handle style - if let styleValue = style, style != .divide { - baseClasses.append( - contentsOf: effectiveEdges.map { edge in - let edgePrefix = edge.rawValue.isEmpty ? "" : "-\(edge.rawValue)" - return "border\(edgePrefix)-\(styleValue.rawValue)" - } - ) - } - - // Handle color - if let colorValue = color?.rawValue { - baseClasses.append("border-\(colorValue)") - } - - let newClasses = combineClasses(baseClasses, withModifiers: modifiers) - - return Element( - tag: self.tag, - id: self.id, - classes: (self.classes ?? []) + newClasses, - role: self.role, - label: self.label, - isSelfClosing: self.isSelfClosing, - customAttributes: self.customAttributes, - content: self.contentBuilder - ) - } - - /// Applies border radius to the element. - /// - /// Creates rounded corners with specified size and edge placement. - /// - /// ## Example - /// ```swift - /// Button() { "Sign Up" } - /// .rounded(.full) - /// - /// Input(name: "search") - /// .rounded(.lg, .left) - /// ``` - public func rounded( - _ size: RadiusSize, - _ edge: RadiusSide = .all, - on modifiers: Modifier... - ) -> Element { - let sidePrefix = edge.rawValue.isEmpty ? "" : "-\(edge.rawValue)" - let baseClasses = ["rounded\(sidePrefix)-\(size.rawValue)"] - let newClasses = combineClasses(baseClasses, withModifiers: modifiers) - - return Element( - tag: self.tag, - id: self.id, - classes: (self.classes ?? []) + newClasses, - role: self.role, - label: self.label, - isSelfClosing: self.isSelfClosing, - customAttributes: self.customAttributes, - content: self.contentBuilder - ) - } - - /// Applies outline styling to the element. - /// - /// Adds an outline with specified width, style, and color around the element. - /// - /// ## Example - /// ```swift - /// Button() { "Submit" } - /// .outline(width: 2, style: .solid, color: .blue(._500), on: .focus) - /// ``` - public func outline( - of width: Int? = nil, - style: BorderStyle? = nil, - color: Color? = nil, - on modifiers: Modifier... - ) -> Element { - var baseClasses: [String] = [] - if let widthValue = width { baseClasses.append("outline-\(widthValue)") } - if let styleValue = style, style != .divide { - baseClasses.append("outline-\(styleValue.rawValue)") - } - if let colorValue = color?.rawValue { - baseClasses.append("outline-\(colorValue)") - } - - let newClasses = combineClasses(baseClasses, withModifiers: modifiers) - - return Element( - tag: self.tag, - id: self.id, - classes: (self.classes ?? []) + newClasses, - role: self.role, - label: self.label, - isSelfClosing: self.isSelfClosing, - customAttributes: self.customAttributes, - content: self.contentBuilder - ) - } - - /// Applies shadow styling to the element. - /// - /// Adds a box shadow with specified size and optional color. - /// - /// ## Example - /// ```swift - /// Stack(classes: ["card"]) { - /// Heading(.title) { "Card Title" } - /// Text { "Card content" } - /// } - /// .shadow(of: .md) - /// .shadow(of: .xl, on: .hover) - /// ``` - public func shadow( - of size: ShadowSize, - color: Color? = nil, - on modifiers: Modifier... - ) -> Element { - var baseClasses: [String] = ["shadow-\(size.rawValue)"] - if let colorValue = color?.rawValue { - baseClasses.append("shadow-\(colorValue)") - } - - let newClasses = combineClasses(baseClasses, withModifiers: modifiers) - - return Element( - tag: self.tag, - id: self.id, - classes: (self.classes ?? []) + newClasses, - role: self.role, - label: self.label, - isSelfClosing: self.isSelfClosing, - customAttributes: self.customAttributes, - content: self.contentBuilder - ) - } - - /// Applies a focus ring around the element. - /// - /// Adds a ring with specified size and color, useful for highlighting interactive elements. - /// - /// ## Example - /// ```swift - /// Button() { "Click Me" } - /// .ring(of: 2, color: .blue(._500), on: .focus) - /// ``` - public func ring( - of size: Int = 1, - color: Color? = nil, - on modifiers: Modifier... - ) -> Element { - var baseClasses: [String] = ["ring-\(size)"] - if let colorValue = color?.rawValue { - baseClasses.append("ring-\(colorValue)") - } - - let newClasses = combineClasses(baseClasses, withModifiers: modifiers) - - return Element( - tag: self.tag, - id: self.id, - classes: (self.classes ?? []) + newClasses, - role: self.role, - label: self.label, - isSelfClosing: self.isSelfClosing, - customAttributes: self.customAttributes, - content: self.contentBuilder - ) - } -} diff --git a/Sources/WebUI/Styles/Appearance/Border/BorderStyleOperation.swift b/Sources/WebUI/Styles/Appearance/Border/BorderStyleOperation.swift new file mode 100644 index 00000000..bc2c3970 --- /dev/null +++ b/Sources/WebUI/Styles/Appearance/Border/BorderStyleOperation.swift @@ -0,0 +1,213 @@ +import Foundation + +/// Style operation for border styling +/// +/// Provides a unified implementation for border styling that can be used across +/// Element methods and the Declaritive DSL functions. +public struct BorderStyleOperation: StyleOperation, @unchecked Sendable { + /// Parameters for border styling + public struct Parameters { + /// The border width + public let width: Int? + + /// The edges to apply the border to + public let edges: [Edge] + + /// The border style + public let style: BorderStyle? + + /// The border color + public let color: Color? + + /// Creates parameters for border styling + /// + /// - Parameters: + /// - width: The border width + /// - edges: The edges to apply the border to + /// - style: The border style + /// - color: The border color + public init( + width: Int? = 1, + edges: [Edge] = [.all], + style: BorderStyle? = nil, + color: Color? = nil + ) { + self.width = width + self.edges = edges.isEmpty ? [.all] : edges + self.style = style + self.color = color + } + + /// Creates parameters from a StyleParameters container + /// + /// - Parameter params: The style parameters container + /// - Returns: BorderStyleOperation.Parameters + public static func from(_ params: StyleParameters) -> Parameters { + Parameters( + width: params.get("width"), + edges: params.get("edges", default: [.all]), + style: params.get("style"), + color: params.get("color") + ) + } + } + + /// Applies the border style and returns the appropriate CSS classes + /// + /// - Parameter params: The parameters for border styling + /// - Returns: An array of CSS class names to be applied to elements + public func applyClasses(params: Parameters) -> [String] { + var classes: [String] = [] + let width = params.width + let edges = params.edges + let style = params.style + let color = params.color + + for edge in edges { + if let style = style, style == .divide { + if let width = width { + let divideClass = edge == .horizontal ? "divide-x-\(width)" : "divide-y-\(width)" + classes.append(divideClass) + } + } else { + let prefix = edge == .all ? "border" : "border-\(edge.rawValue)" + if let width = width { + classes.append("\(prefix)-\(width)") + } else { + classes.append(prefix) + } + } + } + + if let style = style, style != .divide { + classes.append("border-\(style.rawValue)") + } + + if let color = color { + classes.append("border-\(color.rawValue)") + } + + return classes + } + + /// Shared instance for use across the framework + public static let shared = BorderStyleOperation() + + /// Private initializer to enforce singleton usage + private init() {} +} + +// Extension for Element to provide border styling +extension Element { + /// Applies border styling to the element with specified attributes. + /// + /// Adds borders with custom width, style, and color to specified edges of an element. + /// + /// - Parameters: + /// - width: The border width in pixels. + /// - edges: One or more edges to apply the border to. Defaults to `.all`. + /// - style: The border style (solid, dashed, etc.). + /// - color: The border color. + /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. + /// - Returns: A new element with updated border classes. + /// + /// ## Example + /// ```swift + /// Stack() + /// .border(of: 2, at: .bottom, color: .blue(._500)) + /// .border(of: 1, at: .horizontal, color: .gray(._200), on: .hover) + /// ``` + public func border( + of width: Int? = nil, + at edges: Edge..., + style: BorderStyle? = nil, + color: Color? = nil, + on modifiers: Modifier... + ) -> Element { + let params = BorderStyleOperation.Parameters( + width: width, + edges: edges, + style: style, + color: color + ) + + return BorderStyleOperation.shared.applyToElement( + self, + params: params, + modifiers: modifiers + ) + } +} + +// Extension for ResponsiveBuilder to provide border styling +extension ResponsiveBuilder { + /// Applies border styling in a responsive context. + /// + /// - Parameters: + /// - width: The border width in pixels. + /// - edges: One or more edges to apply the border to. Defaults to `.all`. + /// - style: The border style (solid, dashed, etc.). + /// - color: The border color. + /// - Returns: The builder for method chaining. + @discardableResult + public func border( + of width: Int? = 1, + at edges: Edge..., + style: BorderStyle? = nil, + color: Color? = nil + ) -> ResponsiveBuilder { + let params = BorderStyleOperation.Parameters( + width: width, + edges: edges, + style: style, + color: color + ) + + return BorderStyleOperation.shared.applyToBuilder(self, params: params) + } + + /// Helper method to apply just a border style. + /// + /// - Parameter style: The border style to apply. + /// - Returns: The builder for method chaining. + @discardableResult + public func border(style: BorderStyle) -> ResponsiveBuilder { + let params = BorderStyleOperation.Parameters(style: style) + return BorderStyleOperation.shared.applyToBuilder(self, params: params) + } + + /// Helper method to apply just a border color. + /// + /// - Parameter color: The border color to apply. + /// - Returns: The builder for method chaining. + @discardableResult + public func border(color: Color) -> ResponsiveBuilder { + let params = BorderStyleOperation.Parameters(color: color) + return BorderStyleOperation.shared.applyToBuilder(self, params: params) + } +} + +// Global function for Declaritive DSL +/// Applies border styling in the responsive context. +/// +/// - Parameters: +/// - width: The border width. +/// - edges: The edges to apply the border to. +/// - style: The border style. +/// - color: The border color. +/// - Returns: A responsive modification for borders. +public func border( + of width: Int? = 1, + at edges: Edge..., + style: BorderStyle? = nil, + color: Color? = nil +) -> ResponsiveModification { + let params = BorderStyleOperation.Parameters( + width: width, + edges: edges, + style: style, + color: color + ) + + return BorderStyleOperation.shared.asModification(params: params) +} diff --git a/Sources/WebUI/Styles/Appearance/Border/BorderTypes.swift b/Sources/WebUI/Styles/Appearance/Border/BorderTypes.swift new file mode 100644 index 00000000..f6dbc478 --- /dev/null +++ b/Sources/WebUI/Styles/Appearance/Border/BorderTypes.swift @@ -0,0 +1,85 @@ +/// Defines sides for applying border radius. +/// +/// Represents individual corners or groups of corners for styling border radius. +/// +/// ## Example +/// ```swift +/// Button() { "Sign Up" } +/// .rounded(.lg, .top) +/// ``` +public enum RadiusSide: String { + /// Applies radius to all corners + case all = "" + /// Applies radius to the top side (top-left and top-right) + case top = "t" + /// Applies radius to the right side (top-right and bottom-right) + case right = "r" + /// Applies radius to the bottom side (bottom-left and bottom-right) + case bottom = "b" + /// Applies radius to the left side (top-left and bottom-left) + case left = "l" + /// Applies radius to the top-left corner + case topLeft = "tl" + /// Applies radius to the top-right corner + case topRight = "tr" + /// Applies radius to the bottom-left corner + case bottomLeft = "bl" + /// Applies radius to the bottom-right corner + case bottomRight = "br" +} + +/// Specifies sizes for border radius. +/// +/// Defines a range of radius values from none to full circular. +/// +/// ## Example +/// ```swift +/// Stack(classes: ["card"]) +/// .rounded(.xl) +/// ``` +public enum RadiusSize: String { + /// No border radius (0) + case none = "none" + /// Extra small radius (0.125rem) + case xs = "xs" + /// Small radius (0.25rem) + case sm = "sm" + /// Medium radius (0.375rem) + case md = "md" + /// Large radius (0.5rem) + case lg = "lg" + /// Extra large radius (0.75rem) + case xl = "xl" + /// 2x large radius (1rem) + case xl2 = "2xl" + /// 3x large radius (1.5rem) + case xl3 = "3xl" + /// Full radius (9999px, circular) + case full = "full" +} + +/// Defines styles for borders and outlines. +/// +/// Provides options for solid, dashed, and other border appearances. +/// +/// ## Example +/// ```swift +/// Stack() +/// .border(width: 1, style: .dashed, color: .gray(._300)) +/// ``` +public enum BorderStyle: String { + /// Solid line border + case solid = "solid" + /// Dashed line border + case dashed = "dashed" + /// Dotted line border + case dotted = "dotted" + /// Double line border + case double = "double" + /// Hidden border (none) + case hidden = "hidden" + /// No border (none) + case none = "none" + /// Divider style for child elements + case divide = "divide" +} diff --git a/Sources/WebUI/Styles/Appearance/BorderRadiusStyleOperation.swift b/Sources/WebUI/Styles/Appearance/BorderRadiusStyleOperation.swift new file mode 100644 index 00000000..86ec1c58 --- /dev/null +++ b/Sources/WebUI/Styles/Appearance/BorderRadiusStyleOperation.swift @@ -0,0 +1,143 @@ +import Foundation + +/// Style operation for border radius styling +/// +/// Provides a unified implementation for border radius styling that can be used across +/// Element methods and the Declarative DSL functions. +public struct BorderRadiusStyleOperation: StyleOperation, @unchecked Sendable { + /// Parameters for border radius styling + public struct Parameters { + /// The border radius size + public let size: RadiusSize? + + /// The sides to apply the radius to + public let sides: [RadiusSide] + + /// Creates parameters for border radius styling + /// + /// - Parameters: + /// - size: The border radius size + /// - sides: The sides to apply the radius to + public init( + size: RadiusSize? = .md, + sides: [RadiusSide] = [.all] + ) { + self.size = size + self.sides = sides.isEmpty ? [.all] : sides + } + + /// Creates parameters from a StyleParameters container + /// + /// - Parameter params: The style parameters container + /// - Returns: BorderRadiusStyleOperation.Parameters + public static func from(_ params: StyleParameters) -> Parameters { + Parameters( + size: params.get("size"), + sides: params.get("sides", default: [.all]) + ) + } + } + + /// Applies the border radius style and returns the appropriate CSS classes + /// + /// - Parameter params: The parameters for border radius styling + /// - Returns: An array of CSS class names to be applied to elements + public func applyClasses(params: Parameters) -> [String] { + var classes: [String] = [] + let size = params.size + let sides = params.sides + + for side in sides { + let sidePrefix = side == .all ? "" : "-\(side.rawValue)" + let sizeValue = size != nil ? "-\(size!.rawValue)" : "" + + classes.append("rounded\(sidePrefix)\(sizeValue)") + } + + return classes + } + + /// Shared instance for use across the framework + public static let shared = BorderRadiusStyleOperation() + + /// Private initializer to enforce singleton usage + private init() {} +} + +// Extension for Element to provide border radius styling +extension Element { + /// Applies border radius styling to the element. + /// + /// - Parameters: + /// - size: The radius size from none to full. + /// - sides: Zero or more sides to apply the radius to. Defaults to all sides. + /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. + /// - Returns: A new element with updated border radius classes. + /// + /// ## Example + /// ```swift + /// Button() { "Sign Up" } + /// .rounded(.lg) + /// + /// Stack(classes: ["card"]) + /// .rounded(.xl, .top) + /// .rounded(.lg, .bottom, on: .hover) + /// ``` + public func rounded( + _ size: RadiusSize? = .md, + _ sides: RadiusSide..., + on modifiers: Modifier... + ) -> Element { + let params = BorderRadiusStyleOperation.Parameters( + size: size, + sides: sides + ) + + return BorderRadiusStyleOperation.shared.applyToElement( + self, + params: params, + modifiers: modifiers + ) + } +} + +// Extension for ResponsiveBuilder to provide border radius styling +extension ResponsiveBuilder { + /// Applies border radius styling in a responsive context. + /// + /// - Parameters: + /// - size: The radius size from none to full. + /// - sides: Zero or more sides to apply the radius to. + /// - Returns: The builder for method chaining. + @discardableResult + public func rounded( + _ size: RadiusSize? = .md, + _ sides: RadiusSide... + ) -> ResponsiveBuilder { + let params = BorderRadiusStyleOperation.Parameters( + size: size, + sides: sides + ) + + return BorderRadiusStyleOperation.shared.applyToBuilder(self, params: params) + } +} + +// Global function for Declarative DSL +/// Applies border radius styling in the responsive context. +/// +/// - Parameters: +/// - size: The radius size from none to full. +/// - sides: The sides to apply the radius to. +/// - Returns: A responsive modification for border radius. +public func rounded( + _ size: RadiusSize? = .md, + _ sides: RadiusSide... +) -> ResponsiveModification { + let params = BorderRadiusStyleOperation.Parameters( + size: size, + sides: sides + ) + + return BorderRadiusStyleOperation.shared.asModification(params: params) +} diff --git a/Sources/WebUI/Styles/Appearance/Color.swift b/Sources/WebUI/Styles/Appearance/Color.swift deleted file mode 100644 index e8c897e3..00000000 --- a/Sources/WebUI/Styles/Appearance/Color.swift +++ /dev/null @@ -1,323 +0,0 @@ -/// Represents a color value for styling HTML elements. -/// -/// Defines a comprehensive palette of colors with standardized shades, optional opacity levels, -/// and a custom option for arbitrary values. These colors map directly to Tailwind CSS -/// color classes when applied to elements. -/// -/// ## Example -/// ```swift -/// Button(type: .submit) { "Save" } -/// .background(color: .blue(.500)) -/// .font(color: .white) -/// ``` -public enum Color { - /// A slate gray color with varying intensity shades and optional opacity. - /// - /// A cool gray with subtle blue undertones. - case slate(Shade, opacity: Double? = nil) - - /// A neutral gray color with varying intensity shades and optional opacity. - /// - /// A pure, balanced gray without strong undertones. - case gray(Shade, opacity: Double? = nil) - - /// A cool-toned gray color with varying intensity shades and optional opacity. - /// - /// A bluish-gray resembling zinc metal. - case zinc(Shade, opacity: Double? = nil) - - /// A balanced neutral color with varying intensity shades and optional opacity. - /// - /// A truly neutral gray without warm or cool undertones. - case neutral(Shade, opacity: Double? = nil) - - /// A warm-toned stone color with varying intensity shades and optional opacity. - /// - /// A brownish-gray resembling natural stone. - case stone(Shade, opacity: Double? = nil) - - /// A vibrant red color with varying intensity shades and optional opacity. - /// - /// A pure red that stands out for attention, warnings, and errors. - case red(Shade, opacity: Double? = nil) - - /// A bright orange color with varying intensity shades and optional opacity. - /// - /// A warm, energetic color between red and yellow. - case orange(Shade, opacity: Double? = nil) - - /// A rich amber color with varying intensity shades and optional opacity. - /// - /// A golden yellow-orange resembling amber gemstones. - case amber(Shade, opacity: Double? = nil) - - /// A sunny yellow color with varying intensity shades and optional opacity. - /// - /// A bright, attention-grabbing primary color. - case yellow(Shade, opacity: Double? = nil) - - /// A fresh lime color with varying intensity shades and optional opacity. - /// - /// A vibrant yellowish-green color. - case lime(Shade, opacity: Double? = nil) - - /// A lush green color with varying intensity shades and optional opacity. - /// - /// A balanced green suitable for success states and environmental themes. - case green(Shade, opacity: Double? = nil) - - /// A deep emerald color with varying intensity shades and optional opacity. - /// - /// A rich green with blue undertones resembling emerald gemstones. - case emerald(Shade, opacity: Double? = nil) - - /// A teal blue-green color with varying intensity shades and optional opacity. - /// - /// A balanced blue-green color with elegant properties. - case teal(Shade, opacity: Double? = nil) - - /// A bright cyan color with varying intensity shades and optional opacity. - /// - /// A vivid blue-green color with high visibility. - case cyan(Shade, opacity: Double? = nil) - - /// A soft sky blue color with varying intensity shades and optional opacity. - /// - /// A light blue resembling a clear sky. - case sky(Shade, opacity: Double? = nil) - - /// A classic blue color with varying intensity shades and optional opacity. - /// - /// A primary blue suitable for interfaces, links, and buttons. - case blue(Shade, opacity: Double? = nil) - - /// A rich indigo color with varying intensity shades and optional opacity. - /// - /// A deep blue-purple resembling indigo dye. - case indigo(Shade, opacity: Double? = nil) - - /// A vibrant violet color with varying intensity shades and optional opacity. - /// - /// A bright purple with strong blue undertones. - case violet(Shade, opacity: Double? = nil) - - /// A deep purple color with varying intensity shades and optional opacity. - /// - /// A rich mixture of red and blue with royal connotations. - case purple(Shade, opacity: Double? = nil) - - /// A bold fuchsia color with varying intensity shades and optional opacity. - /// - /// A vivid purple-red color with high contrast. - case fuchsia(Shade, opacity: Double? = nil) - - /// A soft pink color with varying intensity shades and optional opacity. - /// - /// A light red with warm, gentle appearance. - case pink(Shade, opacity: Double? = nil) - - /// A warm rose color with varying intensity shades and optional opacity. - /// - /// A deep pink resembling rose flowers. - case rose(Shade, opacity: Double? = nil) - - /// A custom color defined by a raw CSS value with optional opacity. - /// - /// Allows using arbitrary color values not included in the standard palette. - /// - /// - Parameters: - /// - String: A valid CSS color value (hex, RGB, HSL, or named color). - /// - opacity: Optional opacity value between 0 and 1. - /// - /// - /// ## Example - /// ```swift - /// Color.custom("#00aabb") - /// Color.custom("rgb(100, 150, 200)", opacity: 0.5) - /// ``` - case custom(String, opacity: Double? = nil) - - /// Defines shade intensity for colors in a consistent scale. - /// - /// Specifies a standardized range of shades from lightest (50) to darkest (950), - /// providing fine-grained control over color intensity. The scale follows a - /// consistent progression where lower numbers are lighter and higher numbers are darker. - /// - /// - /// ## Example - /// ```swift - /// // Light blue background with dark blue text - /// Stack() - /// .background(color: .blue(._100)) - /// .font(color: .blue(._800)) - /// ``` - public enum Shade: Int { - /// The lightest shade (50), typically a very faint tint. - /// - /// Best used for subtle backgrounds, hover states on light themes, or decorative elements. - case _50 = 50 - - /// A light shade (100), slightly more pronounced than 50. - /// - /// Suitable for light backgrounds, hover states, or highlighting selected items. - case _100 = 100 - - /// A subtle shade (200), often used for backgrounds or hover states. - /// - /// Good for secondary backgrounds, alternating rows, or light borders. - case _200 = 200 - - /// A moderate shade (300), suitable for borders or accents. - /// - /// Effective for dividers, borders, or subtle accent elements. - case _300 = 300 - - /// A balanced shade (400), often used for text or UI elements. - /// - /// Works well for secondary text, icons, or medium-emphasis UI elements. - case _400 = 400 - - /// A medium shade (500), commonly the default for a color family. - /// - /// The standard intensity, ideal for primary UI elements like buttons or indicators. - case _500 = 500 - - /// A slightly darker shade (600), good for emphasis. - /// - /// Useful for hover states on colored elements or medium-emphasis text. - case _600 = 600 - - /// A dark shade (700), often used for active states. - /// - /// Effective for active states, pressed buttons, or high-emphasis UI elements. - case _700 = 700 - - /// A very dark shade (800), suitable for strong contrast. - /// - /// Good for high-contrast text on light backgrounds or dark UI elements. - case _800 = 800 - - /// An almost black shade (900), often for deep backgrounds. - /// - /// Excellent for very dark backgrounds or high-contrast text elements. - case _900 = 900 - - /// The darkest shade (950), nearly black with a hint of color. - /// - /// The darkest variant, useful for the most intense applications of a color. - case _950 = 950 - } - - /// Provides the raw CSS class value for the color and opacity. - /// - /// Generates the appropriate string value for use in CSS class names, - /// including opacity formatting when specified. - /// - /// - Returns: A string representing the color in CSS class format. - /// - /// - /// ## Example - /// ```swift - /// let color = Color.blue(._500, opacity: 0.75) - /// let value = color.rawValue // Returns "blue-500/75" - /// ``` - public var rawValue: String { - func formatOpacity(_ opacity: Double?) -> String { - guard let opacity = opacity, (0...1).contains(opacity) else { return "" } - return "/\(Int(opacity * 100))" - } - - switch self { - case .slate(let shade, let opacity): - return "slate-\(shade.rawValue)\(formatOpacity(opacity))" - case .gray(let shade, let opacity): - return "gray-\(shade.rawValue)\(formatOpacity(opacity))" - case .zinc(let shade, let opacity): - return "zinc-\(shade.rawValue)\(formatOpacity(opacity))" - case .neutral(let shade, let opacity): - return "neutral-\(shade.rawValue)\(formatOpacity(opacity))" - case .stone(let shade, let opacity): - return "stone-\(shade.rawValue)\(formatOpacity(opacity))" - case .red(let shade, let opacity): - return "red-\(shade.rawValue)\(formatOpacity(opacity))" - case .orange(let shade, let opacity): - return "orange-\(shade.rawValue)\(formatOpacity(opacity))" - case .amber(let shade, let opacity): - return "amber-\(shade.rawValue)\(formatOpacity(opacity))" - case .yellow(let shade, let opacity): - return "yellow-\(shade.rawValue)\(formatOpacity(opacity))" - case .lime(let shade, let opacity): - return "lime-\(shade.rawValue)\(formatOpacity(opacity))" - case .green(let shade, let opacity): - return "green-\(shade.rawValue)\(formatOpacity(opacity))" - case .emerald(let shade, let opacity): - return "emerald-\(shade.rawValue)\(formatOpacity(opacity))" - case .teal(let shade, let opacity): - return "teal-\(shade.rawValue)\(formatOpacity(opacity))" - case .cyan(let shade, let opacity): - return "cyan-\(shade.rawValue)\(formatOpacity(opacity))" - case .sky(let shade, let opacity): - return "sky-\(shade.rawValue)\(formatOpacity(opacity))" - case .blue(let shade, let opacity): - return "blue-\(shade.rawValue)\(formatOpacity(opacity))" - case .indigo(let shade, let opacity): - return "indigo-\(shade.rawValue)\(formatOpacity(opacity))" - case .violet(let shade, let opacity): - return "violet-\(shade.rawValue)\(formatOpacity(opacity))" - case .purple(let shade, let opacity): - return "purple-\(shade.rawValue)\(formatOpacity(opacity))" - case .fuchsia(let shade, let opacity): - return "fuchsia-\(shade.rawValue)\(formatOpacity(opacity))" - case .pink(let shade, let opacity): - return "pink-\(shade.rawValue)\(formatOpacity(opacity))" - case .rose(let shade, let opacity): - return "rose-\(shade.rawValue)\(formatOpacity(opacity))" - case .custom(let value, let opacity): - let opacityStr = formatOpacity(opacity) - return "[\(value)]\(opacityStr)" - } - } -} - -extension Element { - /// Applies a background color to the element. - /// - /// Adds a background color class based on the provided color and optional modifiers. - /// This method applies Tailwind CSS background color classes to the element. - /// - /// - Parameters: - /// - color: Sets the background color from the color palette or a custom value. - /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. - /// - Returns: A new element with updated background color classes. - /// - /// - /// ## Example - /// ```swift - /// // Simple background color - /// Button() { "Submit" } - /// .background(color: .green(._500)) - /// - /// // Background color with modifiers - /// Button() { "Hover me" } - /// .background(color: .white, on: .dark) - /// .background(color: .blue(._500), on: .hover) - /// ``` - public func background( - color: Color, - on modifiers: Modifier... - ) -> Element { - let baseClass = "bg-\(color.rawValue)" - let newClasses = combineClasses([baseClass], withModifiers: modifiers) - - return Element( - tag: self.tag, - id: self.id, - classes: (self.classes ?? []) + newClasses, - role: self.role, - label: self.label, - isSelfClosing: self.isSelfClosing, - customAttributes: self.customAttributes, - content: self.contentBuilder - ) - } -} diff --git a/Sources/WebUI/Styles/Appearance/Display.swift b/Sources/WebUI/Styles/Appearance/Display.swift deleted file mode 100644 index c5ed22f0..00000000 --- a/Sources/WebUI/Styles/Appearance/Display.swift +++ /dev/null @@ -1,213 +0,0 @@ -/// Defines CSS display types for controlling element rendering. -/// -/// Specifies how an element is displayed in the layout. -public enum DisplayType: String { - /// Makes the element not display at all (removed from layout flow). - case none - /// Standard block element (takes full width, creates new line). - case block - /// Inline element (flows with text, no line breaks). - case inline - /// Hybrid that allows width/height but flows inline. - case inlineBlock = "inline-block" - /// Creates a flex container. - case flex - /// Creates an inline flex container. - case inlineFlex = "inline-flex" - /// Creates a grid container. - case grid - /// Creates an inline grid container. - case inlineGrid = "inline-grid" - /// Creates a table element. - case table - /// Creates a table cell element. - case tableCell = "table-cell" - /// Creates a table row element. - case tableRow = "table-row" -} - -/// Defines justification options for layout alignment. -/// -/// Specifies how items are distributed along the main axis in flexbox or grid layouts. -public enum Justify: String { - /// Aligns items to the start of the horizontal axis. - case start - /// Aligns items to the end of the horizontal axis. - case end - /// Centers items along the horizontal axis. - case center - /// Distributes items with equal space between them. - case between - /// Distributes items with equal space around them. - case around - /// Distributes items with equal space between and around them. - case evenly - - /// Provides the raw CSS class value. - public var rawValue: String { "justify-\(self)" } -} - -/// Represents alignment options for flexbox or grid items. -/// -/// Specifies how items are aligned along the secondary axis in flexbox or grid layouts. -public enum Align: String { - /// Aligns items to the start of the vertical axis - case start - /// Aligns items to the end of the vertical axis - case end - /// Centers items along the vertical axis - case center - /// Aligns items to their baseline - case baseline - /// Stretches items to fill the vertical axis - case stretch - - public var rawValue: String { "items-\(self)" } -} - -/// Represents flexbox direction options. -/// -/// Dictates the direction elements flow in a flexbox layout. -public enum Direction: String { - /// Sets the main axis to horizontal (left to right) - case row = "flex-row" - /// Sets the main axis to vertical (top to bottom) - case column = "flex-col" - /// Sets the main axis to horizontal (right to left) - case rowReverse = "flex-row-reverse" - /// Sets the main axis to vertical (bottom to top) - case colReverse = "flex-col-reverse" -} - -/// Represents a flex grow value; dictates whether the container should fill remaining space -public enum Grow: Int { - /// Indicates the container should not fill remaining space - case zero = 0 - /// Indicates the container should fill remaining space - case one = 1 -} - -extension Element { - /// Sets the CSS display property with optional modifiers. - /// - /// - Parameters: - /// - type: The display type to apply. - /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. - /// - Returns: A new element with updated display classes. - /// - /// ## Example - /// ```swift - /// // Make an element a block - /// Element(tag: "span").display(.block) - /// - /// // Make an element inline-block on hover - /// Element(tag: "div").display(.inlineBlock, on: .hover) - /// - /// // Display as table on medium screens and up - /// Element(tag: "div").display(.table, on: .md) - /// ``` - public func display( - _ type: DisplayType, - on modifiers: Modifier... - ) -> Element { - let displayClass = "display-\(type.rawValue)" - let newClasses = combineClasses([displayClass], withModifiers: modifiers) - - return Element( - tag: self.tag, - id: self.id, - classes: (self.classes ?? []) + newClasses, - role: self.role, - label: self.label, - isSelfClosing: self.isSelfClosing, - customAttributes: self.customAttributes, - content: self.contentBuilder - ) - } - - public func flex( - direction: Direction? = nil, - justify: Justify? = nil, - align: Align? = nil, - grow: Grow? = nil, - on modifiers: Modifier... - ) -> Element { - var baseClasses: [String] = [] - - // Only add "flex" class if we have direction, justify, or align - if direction != nil || justify != nil || align != nil { - baseClasses.append("flex") - } - - if let directionValue = direction?.rawValue { - baseClasses.append(directionValue) - } - if let justifyValue = justify?.rawValue { baseClasses.append(justifyValue) } - if let alignValue = align?.rawValue { baseClasses.append(alignValue) } - if let growValue = grow { baseClasses.append("flex-\(growValue.rawValue)") } - - let newClasses = combineClasses(baseClasses, withModifiers: modifiers) - - return Element( - tag: self.tag, - id: self.id, - classes: (self.classes ?? []) + newClasses, - role: self.role, - label: self.label, - isSelfClosing: self.isSelfClosing, - customAttributes: self.customAttributes, - content: self.contentBuilder - ) - } - - public func grid( - justify: Justify? = nil, - align: Align? = nil, - columns: Int? = nil, - on modifiers: Modifier... - ) -> Element { - var baseClasses: [String] = ["grid"] - if let justifyValue = justify?.rawValue { baseClasses.append(justifyValue) } - if let alignValue = align?.rawValue { baseClasses.append(alignValue) } - if let columnsValue = columns { - baseClasses.append("grid-cols-\(columnsValue)") - } - - let newClasses = combineClasses(baseClasses, withModifiers: modifiers) - - return Element( - tag: self.tag, - id: self.id, - classes: (self.classes ?? []) + newClasses, - role: self.role, - label: self.label, - isSelfClosing: self.isSelfClosing, - customAttributes: self.customAttributes, - content: self.contentBuilder - ) - } - - public func hidden( - _ isHidden: Bool = true, - on modifiers: Modifier... - ) -> Element { - let baseClass = "hidden" - let newClasses: [String] - if isHidden { - newClasses = combineClasses([baseClass], withModifiers: modifiers) - } else { - newClasses = [] - } - - return Element( - tag: self.tag, - id: self.id, - classes: (self.classes ?? []) + newClasses, - role: self.role, - label: self.label, - isSelfClosing: self.isSelfClosing, - customAttributes: self.customAttributes, - content: self.contentBuilder - ) - } -} diff --git a/Sources/WebUI/Styles/Appearance/Display/DisplayStyleOperation.swift b/Sources/WebUI/Styles/Appearance/Display/DisplayStyleOperation.swift new file mode 100644 index 00000000..68bb9a32 --- /dev/null +++ b/Sources/WebUI/Styles/Appearance/Display/DisplayStyleOperation.swift @@ -0,0 +1,103 @@ +import Foundation + +/// Style operation for display styling +/// +/// Provides a unified implementation for display styling that can be used across +/// Element methods and the Declarative DSL functions. +public struct DisplayStyleOperation: StyleOperation, @unchecked Sendable { + /// Parameters for display styling + public struct Parameters { + /// The display type + public let type: DisplayType + + /// Creates parameters for display styling + /// + /// - Parameter type: The display type + public init(type: DisplayType) { + self.type = type + } + + /// Creates parameters from a StyleParameters container + /// + /// - Parameter params: The style parameters container + /// - Returns: DisplayStyleOperation.Parameters + public static func from(_ params: StyleParameters) -> Parameters { + Parameters( + type: params.get("type")! + ) + } + } + + /// Applies the display style and returns the appropriate CSS classes + /// + /// - Parameter params: The parameters for display styling + /// - Returns: An array of CSS class names to be applied to elements + public func applyClasses(params: Parameters) -> [String] { + ["display-\(params.type.rawValue)"] + } + + /// Shared instance for use across the framework + public static let shared = DisplayStyleOperation() + + /// Private initializer to enforce singleton usage + private init() {} +} + +// Extension for Element to provide display styling +extension Element { + /// Sets the CSS display property with optional modifiers. + /// + /// - Parameters: + /// - type: The display type to apply. + /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. + /// - Returns: A new element with updated display classes. + /// + /// ## Example + /// ```swift + /// // Make an element a block + /// Element(tag: "span").display(.block) + /// + /// // Make an element inline-block on hover + /// Element(tag: "div").display(.inlineBlock, on: .hover) + /// + /// // Display as table on medium screens and up + /// Element(tag: "div").display(.table, on: .md) + /// ``` + public func display( + _ type: DisplayType, + on modifiers: Modifier... + ) -> Element { + let params = DisplayStyleOperation.Parameters(type: type) + + return DisplayStyleOperation.shared.applyToElement( + self, + params: params, + modifiers: modifiers + ) + } +} + +// Extension for ResponsiveBuilder to provide display styling +extension ResponsiveBuilder { + /// Sets the CSS display property in a responsive context. + /// + /// - Parameter type: The display type to apply. + /// - Returns: The builder for method chaining. + @discardableResult + public func display(_ type: DisplayType) -> ResponsiveBuilder { + let params = DisplayStyleOperation.Parameters(type: type) + + return DisplayStyleOperation.shared.applyToBuilder(self, params: params) + } +} + +// Global function for Declarative DSL +/// Sets the CSS display property in the responsive context. +/// +/// - Parameter type: The display type to apply. +/// - Returns: A responsive modification for display. +public func display(_ type: DisplayType) -> ResponsiveModification { + let params = DisplayStyleOperation.Parameters(type: type) + + return DisplayStyleOperation.shared.asModification(params: params) +} diff --git a/Sources/WebUI/Styles/Appearance/Display/DisplayTypes.swift b/Sources/WebUI/Styles/Appearance/Display/DisplayTypes.swift new file mode 100644 index 00000000..21e9d6e1 --- /dev/null +++ b/Sources/WebUI/Styles/Appearance/Display/DisplayTypes.swift @@ -0,0 +1,88 @@ +/// Defines CSS display types for controlling element rendering. +/// +/// Specifies how an element is displayed in the layout. +public enum DisplayType: String { + /// Makes the element not display at all (removed from layout flow). + case none + /// Standard block element (takes full width, creates new line). + case block + /// Inline element (flows with text, no line breaks). + case inline + /// Hybrid that allows width/height but flows inline. + case inlineBlock = "inline-block" + /// Creates a flex container. + case flex + /// Creates an inline flex container. + case inlineFlex = "inline-flex" + /// Creates a grid container. + case grid + /// Creates an inline grid container. + case inlineGrid = "inline-grid" + /// Creates a table element. + case table + /// Creates a table cell element. + case tableCell = "table-cell" + /// Creates a table row element. + case tableRow = "table-row" +} + +/// Defines justification options for layout alignment. +/// +/// Specifies how items are distributed along the main axis in flexbox or grid layouts. +public enum Justify: String { + /// Aligns items to the start of the horizontal axis. + case start + /// Aligns items to the end of the horizontal axis. + case end + /// Centers items along the horizontal axis. + case center + /// Distributes items with equal space between them. + case between + /// Distributes items with equal space around them. + case around + /// Distributes items with equal space between and around them. + case evenly + + /// Provides the raw CSS class value. + public var rawValue: String { "justify-\(self)" } +} + +/// Represents alignment options for flexbox or grid items. +/// +/// Specifies how items are aligned along the secondary axis in flexbox or grid layouts. +public enum Align: String { + /// Aligns items to the start of the vertical axis + case start + /// Aligns items to the end of the vertical axis + case end + /// Centers items along the vertical axis + case center + /// Aligns items to their baseline + case baseline + /// Stretches items to fill the vertical axis + case stretch + + public var rawValue: String { "items-\(self)" } +} + +/// Represents flexbox direction options. +/// +/// Dictates the direction elements flow in a flexbox layout. +public enum Direction: String { + /// Sets the main axis to horizontal (left to right) + case row = "flex-row" + /// Sets the main axis to vertical (top to bottom) + case column = "flex-col" + /// Sets the main axis to horizontal (right to left) + case rowReverse = "flex-row-reverse" + /// Sets the main axis to vertical (bottom to top) + case colReverse = "flex-col-reverse" +} + +/// Represents a flex grow value; dictates whether the container should fill remaining space +public enum Grow: Int { + /// Indicates the container should not fill remaining space + case zero = 0 + /// Indicates the container should fill remaining space + case one = 1 +} diff --git a/Sources/WebUI/Styles/Appearance/Display/FlexStyleOperation.swift b/Sources/WebUI/Styles/Appearance/Display/FlexStyleOperation.swift new file mode 100644 index 00000000..f40e74bd --- /dev/null +++ b/Sources/WebUI/Styles/Appearance/Display/FlexStyleOperation.swift @@ -0,0 +1,259 @@ +import Foundation + +/// Style operation for flex container styling +/// +/// Provides a unified implementation for flex styling that can be used across +/// Element methods and the Declarative DSL functions. +public struct FlexStyleOperation: StyleOperation, @unchecked Sendable { + /// Parameters for flex styling + public struct Parameters { + /// The flex direction (row, column, etc.) + public let direction: FlexDirection? + + /// The justify content property (start, center, between, etc.) + public let justify: FlexJustify? + + /// The align items property (start, center, end, etc.) + public let align: FlexAlign? + + /// The flex grow property + public let grow: FlexGrow? + + /// Creates parameters for flex styling + /// + /// - Parameters: + /// - direction: The flex direction + /// - justify: The justify content property + /// - align: The align items property + /// - grow: The flex grow property + public init( + direction: FlexDirection? = nil, + justify: FlexJustify? = nil, + align: FlexAlign? = nil, + grow: FlexGrow? = nil + ) { + self.direction = direction + self.justify = justify + self.align = align + self.grow = grow + } + + /// Creates parameters from a StyleParameters container + /// + /// - Parameter params: The style parameters container + /// - Returns: FlexStyleOperation.Parameters + public static func from(_ params: StyleParameters) -> Parameters { + Parameters( + direction: params.get("direction"), + justify: params.get("justify"), + align: params.get("align"), + grow: params.get("grow") + ) + } + } + + /// Applies the flex style and returns the appropriate CSS classes + /// + /// - Parameter params: The parameters for flex styling + /// - Returns: An array of CSS class names to be applied to elements + public func applyClasses(params: Parameters) -> [String] { + var classes = ["flex"] + + if let direction = params.direction { + classes.append("flex-\(direction.rawValue)") + } + + if let justify = params.justify { + classes.append("justify-\(justify.rawValue)") + } + + if let align = params.align { + classes.append("items-\(align.rawValue)") + } + + if let grow = params.grow { + classes.append("flex-\(grow.rawValue)") + } + + return classes + } + + /// Shared instance for use across the framework + public static let shared = FlexStyleOperation() + + /// Private initializer to enforce singleton usage + private init() {} +} + +/// Defines the flex direction +public enum FlexDirection: String { + /// Items are arranged horizontally (left to right) + case row + + /// Items are arranged horizontally in reverse (right to left) + case rowReverse = "row-reverse" + + /// Items are arranged vertically (top to bottom) + case column = "col" + + /// Items are arranged vertically in reverse (bottom to top) + case columnReverse = "col-reverse" +} + +/// Defines how flex items are justified along the main axis +public enum FlexJustify: String { + /// Items are packed at the start of the container + case start + + /// Items are packed at the end of the container + case end + + /// Items are centered along the line + case center + + /// Items are evenly distributed with equal space between them + case between + + /// Items are evenly distributed with equal space around them + case around + + /// Items are evenly distributed with equal space between and around them + case evenly +} + +/// Defines how flex items are aligned along the cross axis +public enum FlexAlign: String { + /// Items are aligned at the start of the cross axis + case start + + /// Items are aligned at the end of the cross axis + case end + + /// Items are centered along the cross axis + case center + + /// Items are stretched to fill the container + case stretch + + /// Items are aligned at the baseline + case baseline +} + +/// Defines the flex grow factor +public enum FlexGrow: String { + /// No growing + case none = "0" + + /// Grow with factor 1 + case one = "1" + + /// Grow with factor 2 + case two = "2" + + /// Grow with factor 3 + case three = "3" + + /// Grow with factor 4 + case four = "4" + + /// Grow with factor 5 + case five = "5" +} + +// Extension for Element to provide flex styling +extension Element { + /// Sets flex container properties with optional modifiers. + /// + /// - Parameters: + /// - direction: The flex direction (row, column, etc.). + /// - justify: How to align items along the main axis. + /// - align: How to align items along the cross axis. + /// - grow: The flex grow factor. + /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. + /// - Returns: A new element with updated flex classes. + /// + /// ## Example + /// ```swift + /// // Create a flex container with column layout + /// Element(tag: "div").flex(direction: .column) + /// + /// // Create a flex container with row layout and centered content + /// Element(tag: "div").flex(direction: .row, justify: .center, align: .center) + /// + /// // Apply flex layout only on medium screens and up + /// Element(tag: "div").flex(direction: .row, on: .md) + /// ``` + public func flex( + direction: FlexDirection? = nil, + justify: FlexJustify? = nil, + align: FlexAlign? = nil, + grow: FlexGrow? = nil, + on modifiers: Modifier... + ) -> Element { + let params = FlexStyleOperation.Parameters( + direction: direction, + justify: justify, + align: align, + grow: grow + ) + + return FlexStyleOperation.shared.applyToElement( + self, + params: params, + modifiers: modifiers + ) + } +} + +// Extension for ResponsiveBuilder to provide flex styling +extension ResponsiveBuilder { + /// Sets flex container properties in a responsive context. + /// + /// - Parameters: + /// - direction: The flex direction (row, column, etc.). + /// - justify: How to align items along the main axis. + /// - align: How to align items along the cross axis. + /// - grow: The flex grow factor. + /// - Returns: The builder for method chaining. + @discardableResult + public func flex( + direction: FlexDirection? = nil, + justify: FlexJustify? = nil, + align: FlexAlign? = nil, + grow: FlexGrow? = nil + ) -> ResponsiveBuilder { + let params = FlexStyleOperation.Parameters( + direction: direction, + justify: justify, + align: align, + grow: grow + ) + + return FlexStyleOperation.shared.applyToBuilder(self, params: params) + } +} + +// Global function for Declarative DSL +/// Sets flex container properties in the responsive context. +/// +/// - Parameters: +/// - direction: The flex direction (row, column, etc.). +/// - justify: How to align items along the main axis. +/// - align: How to align items along the cross axis. +/// - grow: The flex grow factor. +/// - Returns: A responsive modification for flex container. +public func flex( + direction: FlexDirection? = nil, + justify: FlexJustify? = nil, + align: FlexAlign? = nil, + grow: FlexGrow? = nil +) -> ResponsiveModification { + let params = FlexStyleOperation.Parameters( + direction: direction, + justify: justify, + align: align, + grow: grow + ) + + return FlexStyleOperation.shared.asModification(params: params) +} \ No newline at end of file diff --git a/Sources/WebUI/Styles/Appearance/Display/GridStyleOperation.swift b/Sources/WebUI/Styles/Appearance/Display/GridStyleOperation.swift new file mode 100644 index 00000000..10a2d4c4 --- /dev/null +++ b/Sources/WebUI/Styles/Appearance/Display/GridStyleOperation.swift @@ -0,0 +1,219 @@ +import Foundation + +/// Style operation for grid container styling +/// +/// Provides a unified implementation for grid styling that can be used across +/// Element methods and the Declarative DSL functions. +public struct GridStyleOperation: StyleOperation, @unchecked Sendable { + /// Parameters for grid styling + public struct Parameters { + /// The number of grid columns + public let columns: Int? + + /// The number of grid rows + public let rows: Int? + + /// The grid flow direction + public let flow: GridFlow? + + /// The column span value + public let columnSpan: Int? + + /// The row span value + public let rowSpan: Int? + + /// Creates parameters for grid styling + /// + /// - Parameters: + /// - columns: The number of grid columns + /// - rows: The number of grid rows + /// - flow: The grid flow direction + /// - columnSpan: The column span value + /// - rowSpan: The row span value + public init( + columns: Int? = nil, + rows: Int? = nil, + flow: GridFlow? = nil, + columnSpan: Int? = nil, + rowSpan: Int? = nil + ) { + self.columns = columns + self.rows = rows + self.flow = flow + self.columnSpan = columnSpan + self.rowSpan = rowSpan + } + + /// Creates parameters from a StyleParameters container + /// + /// - Parameter params: The style parameters container + /// - Returns: GridStyleOperation.Parameters + public static func from(_ params: StyleParameters) -> Parameters { + Parameters( + columns: params.get("columns"), + rows: params.get("rows"), + flow: params.get("flow"), + columnSpan: params.get("columnSpan"), + rowSpan: params.get("rowSpan") + ) + } + } + + /// Applies the grid style and returns the appropriate CSS classes + /// + /// - Parameter params: The parameters for grid styling + /// - Returns: An array of CSS class names to be applied to elements + public func applyClasses(params: Parameters) -> [String] { + var classes = ["grid"] + + if let columns = params.columns { + classes.append("grid-cols-\(columns)") + } + + if let rows = params.rows { + classes.append("grid-rows-\(rows)") + } + + if let flow = params.flow { + classes.append("grid-flow-\(flow.rawValue)") + } + + if let columnSpan = params.columnSpan { + classes.append("col-span-\(columnSpan)") + } + + if let rowSpan = params.rowSpan { + classes.append("row-span-\(rowSpan)") + } + + return classes + } + + /// Shared instance for use across the framework + public static let shared = GridStyleOperation() + + /// Private initializer to enforce singleton usage + private init() {} +} + +/// Defines the grid flow direction +public enum GridFlow: String { + /// Items flow row by row + case row + + /// Items flow column by column + case col + + /// Items flow row by row, dense packing + case rowDense = "row-dense" + + /// Items flow column by column, dense packing + case colDense = "col-dense" +} + +// Extension for Element to provide grid styling +extension Element { + /// Sets grid container properties with optional modifiers. + /// + /// - Parameters: + /// - columns: The number of grid columns. + /// - rows: The number of grid rows. + /// - flow: The grid flow direction. + /// - columnSpan: The column span value. + /// - rowSpan: The row span value. + /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. + /// - Returns: A new element with updated grid classes. + /// + /// ## Example + /// ```swift + /// // Create a grid container with 3 columns + /// Element(tag: "div").grid(columns: 3) + /// + /// // Create a grid container with 2 columns and 3 rows + /// Element(tag: "div").grid(columns: 2, rows: 3) + /// + /// // Apply grid layout only on medium screens and up + /// Element(tag: "div").grid(columns: 2, on: .md) + /// ``` + public func grid( + columns: Int? = nil, + rows: Int? = nil, + flow: GridFlow? = nil, + columnSpan: Int? = nil, + rowSpan: Int? = nil, + on modifiers: Modifier... + ) -> Element { + let params = GridStyleOperation.Parameters( + columns: columns, + rows: rows, + flow: flow, + columnSpan: columnSpan, + rowSpan: rowSpan + ) + + return GridStyleOperation.shared.applyToElement( + self, + params: params, + modifiers: modifiers + ) + } +} + +// Extension for ResponsiveBuilder to provide grid styling +extension ResponsiveBuilder { + /// Sets grid container properties in a responsive context. + /// + /// - Parameters: + /// - columns: The number of grid columns. + /// - rows: The number of grid rows. + /// - flow: The grid flow direction. + /// - columnSpan: The column span value. + /// - rowSpan: The row span value. + /// - Returns: The builder for method chaining. + @discardableResult + public func grid( + columns: Int? = nil, + rows: Int? = nil, + flow: GridFlow? = nil, + columnSpan: Int? = nil, + rowSpan: Int? = nil + ) -> ResponsiveBuilder { + let params = GridStyleOperation.Parameters( + columns: columns, + rows: rows, + flow: flow, + columnSpan: columnSpan, + rowSpan: rowSpan + ) + + return GridStyleOperation.shared.applyToBuilder(self, params: params) + } +} + +// Global function for Declarative DSL +/// Sets grid container properties in the responsive context. +/// +/// - Parameters: +/// - columns: The number of grid columns. +/// - rows: The number of grid rows. +/// - flow: The grid flow direction. +/// - columnSpan: The column span value. +/// - rowSpan: The row span value. +/// - Returns: A responsive modification for grid container. +public func grid( + columns: Int? = nil, + rows: Int? = nil, + flow: GridFlow? = nil, + columnSpan: Int? = nil, + rowSpan: Int? = nil +) -> ResponsiveModification { + let params = GridStyleOperation.Parameters( + columns: columns, + rows: rows, + flow: flow, + columnSpan: columnSpan, + rowSpan: rowSpan + ) + + return GridStyleOperation.shared.asModification(params: params) +} \ No newline at end of file diff --git a/Sources/WebUI/Styles/Appearance/Display/VisibilityStyleOperation.swift b/Sources/WebUI/Styles/Appearance/Display/VisibilityStyleOperation.swift new file mode 100644 index 00000000..919f01fd --- /dev/null +++ b/Sources/WebUI/Styles/Appearance/Display/VisibilityStyleOperation.swift @@ -0,0 +1,110 @@ +import Foundation + +/// Style operation for visibility styling +/// +/// Provides a unified implementation for visibility styling that can be used across +/// Element methods and the Declarative DSL functions. +public struct VisibilityStyleOperation: StyleOperation, @unchecked Sendable { + /// Parameters for visibility styling + public struct Parameters { + /// Whether the element should be hidden + public let isHidden: Bool + + /// Creates parameters for visibility styling + /// + /// - Parameter isHidden: Whether the element should be hidden + public init(isHidden: Bool = true) { + self.isHidden = isHidden + } + + /// Creates parameters from a StyleParameters container + /// + /// - Parameter params: The style parameters container + /// - Returns: VisibilityStyleOperation.Parameters + public static func from(_ params: StyleParameters) -> Parameters { + Parameters( + isHidden: params.get("isHidden") ?? true + ) + } + } + + /// Applies the visibility style and returns the appropriate CSS classes + /// + /// - Parameter params: The parameters for visibility styling + /// - Returns: An array of CSS class names to be applied to elements + public func applyClasses(params: Parameters) -> [String] { + if params.isHidden { + return ["hidden"] + } else { + return [] + } + } + + /// Shared instance for use across the framework + public static let shared = VisibilityStyleOperation() + + /// Private initializer to enforce singleton usage + private init() {} +} + +// Extension for Element to provide visibility styling +extension Element { + /// Controls the visibility of an element with optional modifiers. + /// + /// - Parameters: + /// - isHidden: Whether the element should be hidden (default: true). + /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. + /// - Returns: A new element with updated visibility classes. + /// + /// ## Example + /// ```swift + /// // Hide an element + /// Element(tag: "div").hidden() + /// + /// // Show an element on hover + /// Element(tag: "div").hidden(on: .hover) + /// + /// // Hide an element on medium screens and up + /// Element(tag: "div").hidden(on: .md) + /// + /// // Make visible on medium screens + /// Element(tag: "div").hidden(false, on: .md) + /// ``` + public func hidden( + _ isHidden: Bool = true, + on modifiers: Modifier... + ) -> Element { + let params = VisibilityStyleOperation.Parameters(isHidden: isHidden) + + return VisibilityStyleOperation.shared.applyToElement( + self, + params: params, + modifiers: modifiers + ) + } +} + +// Extension for ResponsiveBuilder to provide visibility styling +extension ResponsiveBuilder { + /// Controls the visibility of an element in a responsive context. + /// + /// - Parameter isHidden: Whether the element should be hidden (default: true). + /// - Returns: The builder for method chaining. + @discardableResult + public func hidden(_ isHidden: Bool = true) -> ResponsiveBuilder { + let params = VisibilityStyleOperation.Parameters(isHidden: isHidden) + + return VisibilityStyleOperation.shared.applyToBuilder(self, params: params) + } +} + +// Global function for Declarative DSL +/// Controls the visibility of an element in the responsive context. +/// +/// - Parameter isHidden: Whether the element should be hidden (default: true). +/// - Returns: A responsive modification for visibility. +public func hidden(_ isHidden: Bool = true) -> ResponsiveModification { + let params = VisibilityStyleOperation.Parameters(isHidden: isHidden) + + return VisibilityStyleOperation.shared.asModification(params: params) +} \ No newline at end of file diff --git a/Sources/WebUI/Styles/Appearance/Opacity.swift b/Sources/WebUI/Styles/Appearance/Opacity.swift deleted file mode 100644 index 09ac9a27..00000000 --- a/Sources/WebUI/Styles/Appearance/Opacity.swift +++ /dev/null @@ -1,33 +0,0 @@ -extension Element { - /// Sets the opacity of the element with optional modifiers. - /// - /// - Parameters: - /// - value: The opacity value, typically between 0 and 100. - /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. - /// - Returns: A new element with updated opacity classes including applied modifiers. - /// - /// ## Example - /// ```swift - /// Image(source: "/images/profile.jpg", description: "Profile Photo") - /// .opacity(50) - /// .opacity(100, on: .hover) - /// ``` - public func opacity( - _ value: Int, - on modifiers: Modifier... - ) -> Element { - let baseClass = "opacity-\(value)" - let newClasses = combineClasses([baseClass], withModifiers: modifiers) - - return Element( - tag: self.tag, - id: self.id, - classes: (self.classes ?? []) + newClasses, - role: self.role, - label: self.label, - isSelfClosing: self.isSelfClosing, - customAttributes: self.customAttributes, - content: self.contentBuilder - ) - } -} diff --git a/Sources/WebUI/Styles/Appearance/OpacityStyleOperation.swift b/Sources/WebUI/Styles/Appearance/OpacityStyleOperation.swift new file mode 100644 index 00000000..783325f4 --- /dev/null +++ b/Sources/WebUI/Styles/Appearance/OpacityStyleOperation.swift @@ -0,0 +1,98 @@ +import Foundation + +/// Style operation for opacity styling +/// +/// Provides a unified implementation for opacity styling that can be used across +/// Element methods and the Declarative DSL functions. +public struct OpacityStyleOperation: StyleOperation, @unchecked Sendable { + /// Parameters for opacity styling + public struct Parameters { + /// The opacity value (0-100) + public let value: Int + + /// Creates parameters for opacity styling + /// + /// - Parameter value: The opacity value (0-100) + public init(value: Int) { + self.value = value + } + + /// Creates parameters from a StyleParameters container + /// + /// - Parameter params: The style parameters container + /// - Returns: OpacityStyleOperation.Parameters + public static func from(_ params: StyleParameters) -> Parameters { + Parameters( + value: params.get("value")! + ) + } + } + + /// Applies the opacity style and returns the appropriate CSS classes + /// + /// - Parameter params: The parameters for opacity styling + /// - Returns: An array of CSS class names to be applied to elements + public func applyClasses(params: Parameters) -> [String] { + ["opacity-\(params.value)"] + } + + /// Shared instance for use across the framework + public static let shared = OpacityStyleOperation() + + /// Private initializer to enforce singleton usage + private init() {} +} + +// Extension for Element to provide opacity styling +extension Element { + /// Sets the opacity of the element with optional modifiers. + /// + /// - Parameters: + /// - value: The opacity value, typically between 0 and 100. + /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. + /// - Returns: A new element with updated opacity classes including applied modifiers. + /// + /// ## Example + /// ```swift + /// Image(source: "/images/profile.jpg", description: "Profile Photo") + /// .opacity(50) + /// .opacity(100, on: .hover) + /// ``` + public func opacity( + _ value: Int, + on modifiers: Modifier... + ) -> Element { + let params = OpacityStyleOperation.Parameters(value: value) + + return OpacityStyleOperation.shared.applyToElement( + self, + params: params, + modifiers: modifiers + ) + } +} + +// Extension for ResponsiveBuilder to provide opacity styling +extension ResponsiveBuilder { + /// Applies opacity styling in a responsive context. + /// + /// - Parameter value: The opacity value (0-100). + /// - Returns: The builder for method chaining. + @discardableResult + public func opacity(_ value: Int) -> ResponsiveBuilder { + let params = OpacityStyleOperation.Parameters(value: value) + + return OpacityStyleOperation.shared.applyToBuilder(self, params: params) + } +} + +// Global function for Declarative DSL +/// Applies opacity styling in the responsive context. +/// +/// - Parameter value: The opacity value (0-100). +/// - Returns: A responsive modification for opacity. +public func opacity(_ value: Int) -> ResponsiveModification { + let params = OpacityStyleOperation.Parameters(value: value) + + return OpacityStyleOperation.shared.asModification(params: params) +} diff --git a/Sources/WebUI/Styles/Appearance/OutlineStyleOperation.swift b/Sources/WebUI/Styles/Appearance/OutlineStyleOperation.swift new file mode 100644 index 00000000..5d279fc7 --- /dev/null +++ b/Sources/WebUI/Styles/Appearance/OutlineStyleOperation.swift @@ -0,0 +1,188 @@ +import Foundation + +/// Style operation for outline styling +/// +/// Provides a unified implementation for outline styling that can be used across +/// Element methods and the Declarative DSL functions. +public struct OutlineStyleOperation: StyleOperation, @unchecked Sendable { + /// Parameters for outline styling + public struct Parameters { + /// The outline width + public let width: Int? + + /// The outline style (solid, dashed, etc.) + public let style: BorderStyle? + + /// The outline color + public let color: Color? + + /// The outline offset + public let offset: Int? + + /// Creates parameters for outline styling + /// + /// - Parameters: + /// - width: The outline width + /// - style: The outline style + /// - color: The outline color + /// - offset: The outline offset + public init( + width: Int? = nil, + style: BorderStyle? = nil, + color: Color? = nil, + offset: Int? = nil + ) { + self.width = width + self.style = style + self.color = color + self.offset = offset + } + + /// Creates parameters from a StyleParameters container + /// + /// - Parameter params: The style parameters container + /// - Returns: OutlineStyleOperation.Parameters + public static func from(_ params: StyleParameters) -> Parameters { + Parameters( + width: params.get("width"), + style: params.get("style"), + color: params.get("color"), + offset: params.get("offset") + ) + } + } + + /// Applies the outline style and returns the appropriate CSS classes + /// + /// - Parameter params: The parameters for outline styling + /// - Returns: An array of CSS class names to be applied to elements + public func applyClasses(params: Parameters) -> [String] { + var classes = [String]() + + if let width = params.width { + classes.append("outline-\(width)") + } + + if let style = params.style { + classes.append("outline-\(style.rawValue)") + } + + if let color = params.color { + classes.append("outline-\(color.rawValue)") + } + + if let offset = params.offset { + classes.append("outline-offset-\(offset)") + } + + if classes.isEmpty { + classes.append("outline") + } + + return classes + } + + /// Shared instance for use across the framework + public static let shared = OutlineStyleOperation() + + /// Private initializer to enforce singleton usage + private init() {} +} + +// Extension for Element to provide outline styling +extension Element { + /// Sets outline properties with optional modifiers. + /// + /// - Parameters: + /// - width: The outline width. + /// - style: The outline style (solid, dashed, etc.). + /// - color: The outline color. + /// - offset: The outline offset in pixels. + /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. + /// - Returns: A new element with updated outline classes. + /// + /// ## Example + /// ```swift + /// // Add a basic outline + /// Element(tag: "div").outline() + /// + /// // Add a 2px outline with color + /// Element(tag: "div").outline(of: 2, color: .blue(._500)) + /// + /// // Add a dashed outline on focus + /// Element(tag: "div").outline(style: .dashed, on: .focus) + /// ``` + public func outline( + of width: Int? = nil, + style: BorderStyle? = nil, + color: Color? = nil, + offset: Int? = nil, + on modifiers: Modifier... + ) -> Element { + let params = OutlineStyleOperation.Parameters( + width: width, + style: style, + color: color, + offset: offset + ) + + return OutlineStyleOperation.shared.applyToElement( + self, + params: params, + modifiers: modifiers + ) + } +} + +// Extension for ResponsiveBuilder to provide outline styling +extension ResponsiveBuilder { + /// Sets outline properties in a responsive context. + /// + /// - Parameters: + /// - width: The outline width. + /// - style: The outline style (solid, dashed, etc.). + /// - color: The outline color. + /// - offset: The outline offset in pixels. + /// - Returns: The builder for method chaining. + @discardableResult + public func outline( + of width: Int? = nil, + style: BorderStyle? = nil, + color: Color? = nil, + offset: Int? = nil + ) -> ResponsiveBuilder { + let params = OutlineStyleOperation.Parameters( + width: width, + style: style, + color: color, + offset: offset + ) + + return OutlineStyleOperation.shared.applyToBuilder(self, params: params) + } +} + +// Global function for Declarative DSL +/// Sets outline properties in the responsive context. +/// +/// - Parameters: +/// - width: The outline width. +/// - style: The outline style (solid, dashed, etc.). +/// - color: The outline color. +/// - offset: The outline offset in pixels. +/// - Returns: A responsive modification for outline. +public func outline( + of width: Int? = nil, + style: BorderStyle? = nil, + color: Color? = nil, + offset: Int? = nil +) -> ResponsiveModification { + let params = OutlineStyleOperation.Parameters( + width: width, + style: style, + color: color, + offset: offset + ) + + return OutlineStyleOperation.shared.asModification(params: params) +} \ No newline at end of file diff --git a/Sources/WebUI/Styles/Appearance/RingStyleOperation.swift b/Sources/WebUI/Styles/Appearance/RingStyleOperation.swift new file mode 100644 index 00000000..7fe91b7b --- /dev/null +++ b/Sources/WebUI/Styles/Appearance/RingStyleOperation.swift @@ -0,0 +1,160 @@ +// +// RingStyleOperation.swift +// web-ui +// +// Created by Mac Long on 2025.05.22. +// + +import Foundation + +/// Style operation for ring styling +/// +/// Provides a unified implementation for ring styling that can be used across +/// Element methods and the Declaritive DSL functions. +public struct RingStyleOperation: StyleOperation, @unchecked Sendable { + /// Parameters for ring styling + public struct Parameters { + /// The ring width + public let size: Int? + + /// The ring color + public let color: Color? + + /// Creates parameters for ring styling + /// + /// - Parameters: + /// - size: the width of the ring + /// - color: The ring color + public init( + size: Int? = 1, + color: Color? = nil + ) { + self.size = size + self.color = color + } + + /// Creates parameters from a StyleParameters container + /// + /// - Parameter params: The style parameters container + /// - Returns: RingStyleOperation.Parameters + public static func from(_ params: StyleParameters) -> Parameters { + Parameters( + size: params.get("size"), + color: params.get("color") + ) + } + } + + /// Applies the ring style and returns the appropriate CSS classes + /// + /// - Parameter params: The parameters for ring styling + /// - Returns: An array of CSS class names to be applied to elements + public func applyClasses(params: Parameters) -> [String] { + var classes: [String] = [] + let size = params.size ?? 1 + let color = params.color + + classes.append("ring-\(size)") + + if let color = color { + classes.append("ring-\(color.rawValue)") + } + + return classes + } + + /// Shared instance for use across the framework + public static let shared = RingStyleOperation() + + /// Private initializer to enforce singleton usage + private init() {} +} + +// Extension for Element to provide ring styling +extension Element { + /// Applies ring styling to the element with specified attributes. + /// + /// Adds rings with custom width, style, and color to specified edges of an element. + /// + /// - Parameters: + /// - width: The ring width in pixels. + /// - edges: One or more edges to apply the ring to. Defaults to `.all`. + /// - style: The ring style (solid, dashed, etc.). + /// - color: The ring color. + /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. + /// - Returns: A new element with updated ring classes. + /// + /// ## Example + /// ```swift + /// Stack() + /// .ring(of: 2, at: .bottom, color: .blue(._500)) + /// .ring(of: 1, at: .horizontal, color: .gray(._200), on: .hover) + /// ``` + public func ring( + size: Int = 1, + color: Color? = nil, + on modifiers: Modifier... + ) -> Element { + let params = RingStyleOperation.Parameters( + size: size, + color: color + ) + + return RingStyleOperation.shared.applyToElement( + self, + params: params, + modifiers: modifiers + ) + } +} + +// Extension for ResponsiveBuilder to provide ring styling +extension ResponsiveBuilder { + /// Applies ring styling in a responsive context. + /// + /// - Parameters: + /// - size: The width of the ring. + /// - color: The ring color. + /// - Returns: The builder for method chaining. + @discardableResult + public func ring( + size: Int = 1, + color: Color? = nil + ) -> ResponsiveBuilder { + let params = RingStyleOperation.Parameters( + size: size, + color: color + ) + + return RingStyleOperation.shared.applyToBuilder(self, params: params) + } + + /// Helper method to apply just a ring color. + /// + /// - Parameter color: The ring color to apply. + /// - Returns: The builder for method chaining. + @discardableResult + public func ring(color: Color) -> ResponsiveBuilder { + let params = RingStyleOperation.Parameters(color: color) + return RingStyleOperation.shared.applyToBuilder(self, params: params) + } +} + +// Global function for Declaritive DSL +/// Applies ring styling in the responsive context. +/// +/// - Parameters: +/// - size: The width of the ring. +/// - color: The ring color. +/// - Returns: A responsive modification for rings. +public func ring( + of size: Int = 1, + color: Color? = nil +) -> ResponsiveModification { + let params = RingStyleOperation.Parameters( + size: size, + color: color + ) + + return RingStyleOperation.shared.asModification(params: params) +} diff --git a/Sources/WebUI/Styles/Appearance/Shadow/ShadowStyleOperation.swift b/Sources/WebUI/Styles/Appearance/Shadow/ShadowStyleOperation.swift new file mode 100644 index 00000000..8f69cb35 --- /dev/null +++ b/Sources/WebUI/Styles/Appearance/Shadow/ShadowStyleOperation.swift @@ -0,0 +1,155 @@ +import Foundation + +/// Style operation for box shadow styling +/// +/// Provides a unified implementation for box shadow styling that can be used across +/// Element methods and the Declaritive DSL functions. +public struct ShadowStyleOperation: StyleOperation, @unchecked Sendable { + /// Parameters for shadow styling + public struct Parameters { + /// The shadow size + public let size: ShadowSize? + + /// The shadow color + public let color: Color? + + /// Creates parameters for shadow styling + /// + /// - Parameters: + /// - size: The shadow size + /// - color: The shadow color + public init( + size: ShadowSize? = nil, + color: Color? = nil + ) { + self.size = size + self.color = color + } + + /// Creates parameters from a StyleParameters container + /// + /// - Parameter params: The style parameters container + /// - Returns: ShadowStyleOperation.Parameters + public static func from(_ params: StyleParameters) -> Parameters { + Parameters( + size: params.get("size"), + color: params.get("color") + ) + } + } + + /// Applies the shadow style and returns the appropriate CSS classes + /// + /// - Parameter params: The parameters for shadow styling + /// - Returns: An array of CSS class names to be applied to elements + public func applyClasses(params: Parameters) -> [String] { + var classes: [String] = [] + let size = params.size ?? ShadowSize.md + let color = params.color + + classes.append("shadow-\(size.rawValue)") + + if let color = color { + classes.append("shadow-\(color.rawValue)") + } + + return classes + } + + /// Shared instance for use across the framework + public static let shared = ShadowStyleOperation() + + /// Private initializer to enforce singleton usage + private init() {} +} + +// Extension for Element to provide shadow styling +extension Element { + /// Applies shadow styling to the element with specified attributes. + /// + /// Adds shadows with custom size and color to an element. + /// + /// - Parameters: + /// - size: The shadow size (sm, md, lg). + /// - color: The shadow color. + /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. + /// - Returns: A new element with updated shadow classes. + /// + /// ## Example + /// ```swift + /// Stack() + /// .shadow(size: .lg, color: .blue(._500)) + /// .shadow(size: .sm, color: .gray(._200), on: .hover) + /// ``` + public func shadow( + size: ShadowSize, + color: Color? = nil, + on modifiers: Modifier... + ) -> Element { + let params = ShadowStyleOperation.Parameters( + size: size, + color: color + ) + + return ShadowStyleOperation.shared.applyToElement( + self, + params: params, + modifiers: modifiers + ) + } +} + +// Extension for ResponsiveBuilder to provide shadow styling +extension ResponsiveBuilder { + /// Applies shadow styling in a responsive context. + /// + /// - Parameters: + /// - size: The shadow size. + /// - color: The shadow color. + /// - Returns: The builder for method chaining. + @discardableResult + public func shadow( + size: ShadowSize? = .md, + color: Color? = nil, + ) -> ResponsiveBuilder { + let params = ShadowStyleOperation.Parameters( + size: size, + color: color + ) + + return ShadowStyleOperation.shared.applyToBuilder(self, params: params) + } + + /// Helper method to apply just a shadow color. + /// + /// - Parameter color: The shadow color to apply. + /// - Returns: The builder for method chaining. + @discardableResult + public func shadow(color: Color) -> ResponsiveBuilder { + let params = ShadowStyleOperation.Parameters( + size: .md, + color: color + ) + + return ShadowStyleOperation.shared.applyToBuilder(self, params: params) + } +} + +// Global function for Declaritive DSL +/// Applies shadow styling in the responsive context. +/// +/// - Parameters: +/// - size: The shadow size. +/// - color: The shadow color. +/// - Returns: A responsive modification for shadows. +public func shadow( + of size: ShadowSize? = .md, + color: Color? = nil +) -> ResponsiveModification { + let params = ShadowStyleOperation.Parameters( + size: size, + color: color + ) + + return ShadowStyleOperation.shared.asModification(params: params) +} diff --git a/Sources/WebUI/Styles/Appearance/Shadow/ShadowTypes.swift b/Sources/WebUI/Styles/Appearance/Shadow/ShadowTypes.swift new file mode 100644 index 00000000..5f97b5b0 --- /dev/null +++ b/Sources/WebUI/Styles/Appearance/Shadow/ShadowTypes.swift @@ -0,0 +1,27 @@ +/// Specifies sizes for box shadows. +/// +/// Defines shadow sizes from none to extra-large. +/// +/// ## Example +/// ```swift +/// Stack(classes: ["card"]) +/// .shadow(size: .lg, color: .gray(._300, opacity: 0.5)) +/// ``` +public enum ShadowSize: String { + /// No shadow + case none = "none" + /// Extra small shadow (2xs) + case xs2 = "2xs" + /// Extra small shadow (xs) + case xs = "xs" + /// Small shadow (sm) + case sm = "sm" + /// Medium shadow (default) + case md = "md" + /// Large shadow (lg) + case lg = "lg" + /// Extra large shadow (xl) + case xl = "xl" + /// 2x large shadow (2xl) + case xl2 = "2xl" +} diff --git a/Sources/WebUI/Styles/Base/Cursor.swift b/Sources/WebUI/Styles/Base/Cursor.swift deleted file mode 100644 index 119f8b45..00000000 --- a/Sources/WebUI/Styles/Base/Cursor.swift +++ /dev/null @@ -1,52 +0,0 @@ -/// Represents cursor types. -/// -/// ## Example -/// ```swift -/// Button() { "Click Me" } -/// .cursor(.pointer) -/// ``` -public enum CursorType: String { - case auto - case `default` - case pointer - case wait - case text - case move - case notAllowed = "not-allowed" -} - -extension Element { - /// Sets the cursor style of the element with optional modifiers. - /// - /// - Parameters: - /// - type: The cursor type. - /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. - /// - Returns: A new element with updated cursor classes. - /// - /// ## Example - /// ```swift - /// Link(to: "/contact") { "Contact Us" } - /// .cursor(.pointer) - /// - /// Button() - /// .cursor(.notAllowed, on: .hover) - /// ``` - public func cursor( - _ type: CursorType, - on modifiers: Modifier... - ) -> Element { - let baseClass = "cursor-\(type.rawValue)" - let newClasses = combineClasses([baseClass], withModifiers: modifiers) - - return Element( - tag: self.tag, - id: self.id, - classes: (self.classes ?? []) + newClasses, - role: self.role, - label: self.label, - isSelfClosing: self.isSelfClosing, - customAttributes: self.customAttributes, - content: self.contentBuilder - ) - } -} diff --git a/Sources/WebUI/Styles/Base/CursorStyleOperation.swift b/Sources/WebUI/Styles/Base/CursorStyleOperation.swift new file mode 100644 index 00000000..a96685f6 --- /dev/null +++ b/Sources/WebUI/Styles/Base/CursorStyleOperation.swift @@ -0,0 +1,110 @@ +import Foundation + +public enum CursorType: String { + case auto + case `default` + case pointer + case wait + case text + case move + case notAllowed = "not-allowed" +} + +/// Style operation for cursor styling +/// +/// Provides a unified implementation for cursor styling that can be used across +/// Element methods and the Declarative DSL functions. +public struct CursorStyleOperation: StyleOperation, @unchecked Sendable { + /// Parameters for cursor styling + public struct Parameters { + /// The cursor type + public let type: CursorType + + /// Creates parameters for cursor styling + /// + /// - Parameter type: The cursor type + public init(type: CursorType) { + self.type = type + } + + /// Creates parameters from a StyleParameters container + /// + /// - Parameter params: The style parameters container + /// - Returns: CursorStyleOperation.Parameters + public static func from(_ params: StyleParameters) -> Parameters { + Parameters( + type: params.get("type")! + ) + } + } + + /// Applies the cursor style and returns the appropriate CSS classes + /// + /// - Parameter params: The parameters for cursor styling + /// - Returns: An array of CSS class names to be applied to elements + public func applyClasses(params: Parameters) -> [String] { + ["cursor-\(params.type.rawValue)"] + } + + /// Shared instance for use across the framework + public static let shared = CursorStyleOperation() + + /// Private initializer to enforce singleton usage + private init() {} +} + +// Extension for Element to provide cursor styling +extension Element { + /// Sets the cursor style of the element with optional modifiers. + /// + /// - Parameters: + /// - type: The cursor type. + /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. + /// - Returns: A new element with updated cursor classes. + /// + /// ## Example + /// ```swift + /// Link(to: "/contact") { "Contact Us" } + /// .cursor(.pointer) + /// + /// Button() + /// .cursor(.notAllowed, on: .hover) + /// ``` + public func cursor( + _ type: CursorType, + on modifiers: Modifier... + ) -> Element { + let params = CursorStyleOperation.Parameters(type: type) + + return CursorStyleOperation.shared.applyToElement( + self, + params: params, + modifiers: modifiers + ) + } +} + +// Extension for ResponsiveBuilder to provide cursor styling +extension ResponsiveBuilder { + /// Applies cursor styling in a responsive context. + /// + /// - Parameter type: The cursor type. + /// - Returns: The builder for method chaining. + @discardableResult + public func cursor(_ type: CursorType) -> ResponsiveBuilder { + let params = CursorStyleOperation.Parameters(type: type) + + return CursorStyleOperation.shared.applyToBuilder(self, params: params) + } +} + +// Global function for Declarative DSL +/// Applies cursor styling in the responsive context. +/// +/// - Parameter type: The cursor type. +/// - Returns: A responsive modification for cursor. +public func cursor(_ type: CursorType) -> ResponsiveModification { + let params = CursorStyleOperation.Parameters(type: type) + + return CursorStyleOperation.shared.asModification(params: params) +} diff --git a/Sources/WebUI/Styles/Base/EdgeInsets.swift b/Sources/WebUI/Styles/Base/EdgeInsets.swift deleted file mode 100644 index 7c449e14..00000000 --- a/Sources/WebUI/Styles/Base/EdgeInsets.swift +++ /dev/null @@ -1,420 +0,0 @@ -import Foundation - -/// Represents inset values for the four edges of an element. -/// -/// This structure provides a convenient way to specify different spacing values -/// for each edge of an element when applying padding, margins, or other edge-based styling. -/// -/// ## Example -/// ```swift -/// Button() { "Save" } -/// .padding(EdgeInsets(top: 4, leading: 6, bottom: 4, trailing: 6)) -/// .margins(EdgeInsets(top: 2, leading: 0, bottom: 2, trailing: 0)) -/// ``` -public struct EdgeInsets { - /// The inset value for the top edge. - public let top: Int - - /// The inset value for the leading (left) edge. - public let leading: Int - - /// The inset value for the bottom edge. - public let bottom: Int - - /// The inset value for the trailing (right) edge. - public let trailing: Int - - /// Creates an EdgeInsets instance with specified values for each edge. - /// - /// - Parameters: - /// - top: The inset value for the top edge. - /// - leading: The inset value for the leading (left) edge. - /// - bottom: The inset value for the bottom edge. - /// - trailing: The inset value for the trailing (right) edge. - /// - /// ## Example - /// ```swift - /// // Card with different padding on each edge - /// Stack() { - /// Text { "Card content with custom padding" } - /// } - /// .padding(EdgeInsets(top: 4, leading: 6, bottom: 8, trailing: 6)) - /// ``` - public init(top: Int, leading: Int, bottom: Int, trailing: Int) { - self.top = top - self.leading = leading - self.bottom = bottom - self.trailing = trailing - } - - /// Creates an EdgeInsets instance with the same value for all edges. - /// - /// - Parameter value: The inset value to apply to all edges. - /// - /// ## Example - /// ```swift - /// // Uniform margins all around - /// Button() { "Save" } - /// .margins(EdgeInsets(all: 4)) - /// ``` - public init(all value: Int) { - self.top = value - self.leading = value - self.bottom = value - self.trailing = value - } - - /// Creates an EdgeInsets instance with separate values for vertical and horizontal edges. - /// - /// - Parameters: - /// - vertical: The inset value for top and bottom edges. - /// - horizontal: The inset value for leading and trailing edges. - /// - /// ## Example - /// ```swift - /// // Different padding for vertical and horizontal edges - /// Stack() { - /// Text { "Content with more horizontal than vertical padding" } - /// } - /// .padding(EdgeInsets(vertical: 2, horizontal: 6)) - /// ``` - public init(vertical: Int, horizontal: Int) { - self.top = vertical - self.leading = horizontal - self.bottom = vertical - self.trailing = horizontal - } -} - -extension Element { - /// Applies padding styling to the element using EdgeInsets. - /// - /// - Parameters: - /// - insets: The EdgeInsets specifying padding values for each edge. - /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. - /// - Returns: A new element with updated padding classes. - /// - /// ## Example - /// ```swift - /// Button() { "Login" } - /// .padding(EdgeInsets(top: 2, leading: 4, bottom: 2, trailing: 4)) - /// .padding(EdgeInsets(vertical: 4, horizontal: 6), on: .hover) - /// ``` - public func padding(_ insets: EdgeInsets, on modifiers: Modifier...) -> Element { - var baseClasses: [String] = [] - - // Add top padding if different from others - baseClasses.append("pt-\(insets.top)") - - // Add leading padding - baseClasses.append("pl-\(insets.leading)") - - // Add bottom padding - baseClasses.append("pb-\(insets.bottom)") - - // Add trailing padding - baseClasses.append("pr-\(insets.trailing)") - - let newClasses = combineClasses(baseClasses, withModifiers: modifiers) - - return Element( - tag: self.tag, - id: self.id, - classes: (self.classes ?? []) + newClasses, - role: self.role, - label: self.label, - isSelfClosing: self.isSelfClosing, - customAttributes: self.customAttributes, - content: self.contentBuilder - ) - } - - /// Applies margin styling to the element using EdgeInsets. - /// - /// - Parameters: - /// - insets: The EdgeInsets specifying margin values for each edge. - /// - auto: Whether to use automatic margins instead of specific lengths. - /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. - /// - Returns: A new element with updated margin classes. - /// - /// ## Example - /// ```swift - /// // Different margins for different edges - /// Stack() { - /// Text { "Content with custom margins" } - /// } - /// .margins(EdgeInsets(top: 8, leading: 4, bottom: 2, trailing: 4)) - /// - /// // Automatic margins for centering - /// Button() { "Centered" } - /// .margins(EdgeInsets(all: 0), auto: true) - /// ``` - public func margins(_ insets: EdgeInsets, auto: Bool = false, on modifiers: Modifier...) -> Element { - var baseClasses: [String] = [] - - if auto { - baseClasses.append("mt-auto") - baseClasses.append("ml-auto") - baseClasses.append("mb-auto") - baseClasses.append("mr-auto") - } else { - // Add top margin - baseClasses.append("mt-\(insets.top)") - - // Add leading margin - baseClasses.append("ml-\(insets.leading)") - - // Add bottom margin - baseClasses.append("mb-\(insets.bottom)") - - // Add trailing margin - baseClasses.append("mr-\(insets.trailing)") - } - - let newClasses = combineClasses(baseClasses, withModifiers: modifiers) - - return Element( - tag: self.tag, - id: self.id, - classes: (self.classes ?? []) + newClasses, - role: self.role, - label: self.label, - isSelfClosing: self.isSelfClosing, - customAttributes: self.customAttributes, - content: self.contentBuilder - ) - } - - /// Applies border width styling to the element using EdgeInsets. - /// - /// - Parameters: - /// - insets: The EdgeInsets specifying border width values for each edge. - /// - style: The border style to apply. - /// - color: The border color to apply. - /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. - /// - Returns: A new element with updated border classes. - /// - /// ## Example - /// ```swift - /// // Card with different border widths on each side - /// Stack() { - /// Text { "Card with custom borders" } - /// } - /// .border(EdgeInsets(top: 1, leading: 2, bottom: 3, trailing: 2), - /// style: .solid, - /// color: .gray(._300)) - /// - /// // Highlight on hover with thicker bottom border - /// Link(to: "#") { "Hover me" } - /// .border(EdgeInsets(top: 0, leading: 0, bottom: 2, trailing: 0), - /// color: .blue(._500), - /// on: .hover) - /// ``` - public func border( - _ insets: EdgeInsets, - style: BorderStyle? = nil, - color: Color? = nil, - on modifiers: Modifier... - ) -> Element { - var baseClasses: [String] = [] - - // Add top border width - baseClasses.append("border-t-\(insets.top)") - - // Add leading border width - baseClasses.append("border-l-\(insets.leading)") - - // Add bottom border width - baseClasses.append("border-b-\(insets.bottom)") - - // Add trailing border width - baseClasses.append("border-r-\(insets.trailing)") - - // Add border style if provided - if let style = style { - baseClasses.append("border-\(style.rawValue)") - } - - // Add border color if provided - if let color = color { - baseClasses.append("border-\(color.rawValue)") - } - - let newClasses = combineClasses(baseClasses, withModifiers: modifiers) - - return Element( - tag: self.tag, - id: self.id, - classes: (self.classes ?? []) + newClasses, - role: self.role, - label: self.label, - isSelfClosing: self.isSelfClosing, - customAttributes: self.customAttributes, - content: self.contentBuilder - ) - } - - /// Applies position offset styling to the element using EdgeInsets. - /// - /// - Parameters: - /// - type: The position type to apply (static, relative, absolute, fixed, sticky). - /// - insets: The EdgeInsets specifying position offset values for each edge. - /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. - /// - Returns: A new element with updated position classes. - /// - /// ## Example - /// ```swift - /// // Position element absolutely with custom insets - /// Stack() { - /// Text { "Positioned content" } - /// } - /// .position(.absolute, insets: EdgeInsets(top: 2, leading: 4, bottom: 0, trailing: 0)) - /// - /// // Sticky header with top offset - /// Stack() { - /// Text { "Sticky Header" } - /// } - /// .position(.sticky, insets: EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) - /// ``` - public func position(_ type: PositionType? = nil, insets: EdgeInsets, on modifiers: Modifier...) -> Element { - var baseClasses: [String] = [] - - // Add position type if provided - if let type = type { - baseClasses.append(type.rawValue) - } - - // Add top position - baseClasses.append("top-\(insets.top)") - - // Add left position (leading) - baseClasses.append("left-\(insets.leading)") - - // Add bottom position - baseClasses.append("bottom-\(insets.bottom)") - - // Add right position (trailing) - baseClasses.append("right-\(insets.trailing)") - - let newClasses = combineClasses(baseClasses, withModifiers: modifiers) - - return Element( - tag: self.tag, - id: self.id, - classes: (self.classes ?? []) + newClasses, - role: self.role, - label: self.label, - isSelfClosing: self.isSelfClosing, - customAttributes: self.customAttributes, - content: self.contentBuilder - ) - } -} - -extension ResponsiveBuilder { - /// Applies padding styling to the element using EdgeInsets. - /// - /// - Parameter insets: The EdgeInsets specifying padding values for each edge. - /// - Returns: The ResponsiveBuilder for method chaining. - /// - /// ## Example - /// ```swift - /// ResponsiveBuilder() - /// .padding(EdgeInsets(top: 4, leading: 6, bottom: 4, trailing: 6)) - /// .font(size: .lg) - /// ``` - @discardableResult - public func padding(_ insets: EdgeInsets) -> ResponsiveBuilder { - padding(of: insets.top, at: .top) - padding(of: insets.leading, at: .leading) - padding(of: insets.bottom, at: .bottom) - padding(of: insets.trailing, at: .trailing) - return self - } - - /// Applies margin styling to the element using EdgeInsets. - /// - /// - Parameters: - /// - insets: The EdgeInsets specifying margin values for each edge. - /// - auto: Whether to use automatic margins instead of specific lengths. - /// - Returns: The ResponsiveBuilder for method chaining. - /// - /// ## Example - /// ```swift - /// ResponsiveBuilder() - /// .margins(EdgeInsets(vertical: 2, horizontal: 4)) - /// .background(color: .blue(._500)) - /// ``` - @discardableResult - public func margins(_ insets: EdgeInsets, auto: Bool = false) -> ResponsiveBuilder { - if auto { - margins(of: nil, at: .top, auto: true) - margins(of: nil, at: .leading, auto: true) - margins(of: nil, at: .bottom, auto: true) - margins(of: nil, at: .trailing, auto: true) - } else { - margins(of: insets.top, at: .top) - margins(of: insets.leading, at: .leading) - margins(of: insets.bottom, at: .bottom) - margins(of: insets.trailing, at: .trailing) - } - return self - } - - /// Applies border width styling to the element using EdgeInsets. - /// - /// - Parameters: - /// - insets: The EdgeInsets specifying border width values for each edge. - /// - style: The border style to apply. - /// - color: The border color to apply. - /// - Returns: The ResponsiveBuilder for method chaining. - /// - /// ## Example - /// ```swift - /// ResponsiveBuilder() - /// .border(EdgeInsets(top: 1, leading: 0, bottom: 1, trailing: 0), - /// style: .solid, - /// color: .gray(._200)) - /// .rounded(.md) - /// ``` - @discardableResult - public func border(_ insets: EdgeInsets, style: BorderStyle? = nil, color: Color? = nil) -> ResponsiveBuilder { - border(of: insets.top, at: .top) - border(of: insets.leading, at: .leading) - border(of: insets.bottom, at: .bottom) - border(of: insets.trailing, at: .trailing) - - if let style = style { - border(style: style) - } - - if let color = color { - border(color: color) - } - - return self - } - - /// Applies position offset styling to the element using EdgeInsets. - /// - /// - Parameters: - /// - type: The position type to apply (static, relative, absolute, fixed, sticky). - /// - insets: The EdgeInsets specifying position offset values for each edge. - /// - Returns: The ResponsiveBuilder for method chaining. - /// - /// ## Example - /// ```swift - /// ResponsiveBuilder() - /// .position(.relative, insets: EdgeInsets(top: 2, leading: 0, bottom: 0, trailing: 0)) - /// .zIndex(10) - /// ``` - @discardableResult - public func position(_ type: PositionType? = nil, insets: EdgeInsets) -> ResponsiveBuilder { - position(type, at: .top, offset: insets.top) - position(type, at: .leading, offset: insets.leading) - position(type, at: .bottom, offset: insets.bottom) - position(type, at: .trailing, offset: insets.trailing) - - return self - } -} diff --git a/Sources/WebUI/Styles/Base/Font/FontStyleOperation.swift b/Sources/WebUI/Styles/Base/Font/FontStyleOperation.swift new file mode 100644 index 00000000..38de8629 --- /dev/null +++ b/Sources/WebUI/Styles/Base/Font/FontStyleOperation.swift @@ -0,0 +1,268 @@ +import Foundation + +/// Style operation for font styling +/// +/// Provides a unified implementation for font styling that can be used across +/// Element methods and the Declarative DSL functions. +public struct FontStyleOperation: StyleOperation, @unchecked Sendable { + /// Parameters for font styling + public struct Parameters { + /// The font size + public let size: TextSize? + + /// The font weight + public let weight: Weight? + + /// The text alignment + public let alignment: Alignment? + + /// The letter spacing + public let tracking: Tracking? + + /// The line height + public let leading: Leading? + + /// The text decoration + public let decoration: Decoration? + + /// The text wrapping behavior + public let wrapping: Wrapping? + + /// The text color + public let color: Color? + + /// The font family + public let family: String? + + /// Creates parameters for font styling + /// + /// - Parameters: + /// - size: The font size + /// - weight: The font weight + /// - alignment: The text alignment + /// - tracking: The letter spacing + /// - leading: The line height + /// - decoration: The text decoration + /// - wrapping: The text wrapping behavior + /// - color: The text color + /// - family: The font family + public init( + size: TextSize? = nil, + weight: Weight? = nil, + alignment: Alignment? = nil, + tracking: Tracking? = nil, + leading: Leading? = nil, + decoration: Decoration? = nil, + wrapping: Wrapping? = nil, + color: Color? = nil, + family: String? = nil + ) { + self.size = size + self.weight = weight + self.alignment = alignment + self.tracking = tracking + self.leading = leading + self.decoration = decoration + self.wrapping = wrapping + self.color = color + self.family = family + } + + /// Creates parameters from a StyleParameters container + /// + /// - Parameter params: The style parameters container + /// - Returns: FontStyleOperation.Parameters + public static func from(_ params: StyleParameters) -> Parameters { + Parameters( + size: params.get("size"), + weight: params.get("weight"), + alignment: params.get("alignment"), + tracking: params.get("tracking"), + leading: params.get("leading"), + decoration: params.get("decoration"), + wrapping: params.get("wrapping"), + color: params.get("color"), + family: params.get("family") + ) + } + } + + /// Applies the font style and returns the appropriate CSS classes + /// + /// - Parameter params: The parameters for font styling + /// - Returns: An array of CSS class names to be applied to elements + public func applyClasses(params: Parameters) -> [String] { + var classes: [String] = [] + + if let size = params.size { classes.append(size.className) } + if let weight = params.weight { classes.append(weight.className) } + if let alignment = params.alignment { classes.append(alignment.className) } + if let tracking = params.tracking { classes.append(tracking.className) } + if let leading = params.leading { classes.append(leading.className) } + if let decoration = params.decoration { classes.append(decoration.className) } + if let wrapping = params.wrapping { classes.append(wrapping.className) } + if let color = params.color { classes.append("text-\(color.rawValue)") } + if let family = params.family { classes.append("font-[\(family)]") } + + return classes + } + + /// Shared instance for use across the framework + public static let shared = FontStyleOperation() + + /// Private initializer to enforce singleton usage + private init() {} +} + +// Extension for Element to provide font styling +extension Element { + /// Applies font styling to the element with optional modifiers. + /// + /// This comprehensive method allows controlling all aspects of typography including + /// size, weight, alignment, spacing, color, and font family. Each parameter targets + /// a specific aspect of text appearance, and can be combined with modifiers for + /// responsive or state-based typography. + /// + /// - Parameters: + /// - size: The font size from extra-small to extra-large variants. + /// - weight: The font weight from thin to black/heavy. + /// - alignment: The text alignment (left, center, right). + /// - tracking: The letter spacing (character spacing). + /// - leading: The line height (vertical spacing between lines). + /// - decoration: The text decoration style (underline, strikethrough, etc.). + /// - wrapping: The text wrapping behavior. + /// - color: The text color from the color palette. + /// - family: The font family name or stack (e.g., "sans-serif"). + /// - modifiers: Zero or more modifiers to scope the styles (e.g., responsive breakpoints or states). + /// - Returns: A new element with updated font styling classes. + /// + /// ## Example + /// ```swift + /// Text { "Welcome to our site" } + /// .font( + /// size: .xl2, + /// weight: .bold, + /// alignment: .center, + /// tracking: .wide, + /// color: .blue(._600) + /// ) + /// + /// // Responsive typography + /// Heading(.one) { "Responsive Title" } + /// .font(size: .xl3) + /// .font(size: .xl5, on: .lg) // Larger on desktop + /// ``` + public func font( + size: TextSize? = nil, + weight: Weight? = nil, + alignment: Alignment? = nil, + tracking: Tracking? = nil, + leading: Leading? = nil, + decoration: Decoration? = nil, + wrapping: Wrapping? = nil, + color: Color? = nil, + family: String? = nil, + on modifiers: Modifier... + ) -> Element { + let params = FontStyleOperation.Parameters( + size: size, + weight: weight, + alignment: alignment, + tracking: tracking, + leading: leading, + decoration: decoration, + wrapping: wrapping, + color: color, + family: family + ) + + return FontStyleOperation.shared.applyToElement( + self, + params: params, + modifiers: modifiers + ) + } +} + +// Extension for ResponsiveBuilder to provide font styling +extension ResponsiveBuilder { + /// Applies font styling in a responsive context. + /// + /// - Parameters: + /// - size: The font size. + /// - weight: The font weight. + /// - alignment: The text alignment. + /// - tracking: The letter spacing. + /// - leading: The line height. + /// - decoration: The text decoration. + /// - wrapping: The text wrapping behavior. + /// - color: The text color. + /// - family: The font family name. + /// - Returns: The builder for method chaining. + @discardableResult + public func font( + size: TextSize? = nil, + weight: Weight? = nil, + alignment: Alignment? = nil, + tracking: Tracking? = nil, + leading: Leading? = nil, + decoration: Decoration? = nil, + wrapping: Wrapping? = nil, + color: Color? = nil, + family: String? = nil + ) -> ResponsiveBuilder { + let params = FontStyleOperation.Parameters( + size: size, + weight: weight, + alignment: alignment, + tracking: tracking, + leading: leading, + decoration: decoration, + wrapping: wrapping, + color: color, + family: family + ) + + return FontStyleOperation.shared.applyToBuilder(self, params: params) + } +} + +// Global function for Declarative DSL +/// Applies font styling in the responsive context. +/// +/// - Parameters: +/// - size: The font size. +/// - weight: The font weight. +/// - alignment: The text alignment. +/// - tracking: The letter spacing. +/// - leading: The line height. +/// - decoration: The text decoration. +/// - wrapping: The text wrapping behavior. +/// - color: The text color. +/// - family: The font family name. +/// - Returns: A responsive modification for font styling. +public func font( + size: TextSize? = nil, + weight: Weight? = nil, + alignment: Alignment? = nil, + tracking: Tracking? = nil, + leading: Leading? = nil, + decoration: Decoration? = nil, + wrapping: Wrapping? = nil, + color: Color? = nil, + family: String? = nil +) -> ResponsiveModification { + let params = FontStyleOperation.Parameters( + size: size, + weight: weight, + alignment: alignment, + tracking: tracking, + leading: leading, + decoration: decoration, + wrapping: wrapping, + color: color, + family: family + ) + + return FontStyleOperation.shared.asModification(params: params) +} diff --git a/Sources/WebUI/Styles/Base/Typography.swift b/Sources/WebUI/Styles/Base/Font/FontTypes.swift similarity index 60% rename from Sources/WebUI/Styles/Base/Typography.swift rename to Sources/WebUI/Styles/Base/Font/FontTypes.swift index d96e10fc..cdbca154 100644 --- a/Sources/WebUI/Styles/Base/Typography.swift +++ b/Sources/WebUI/Styles/Base/Font/FontTypes.swift @@ -241,139 +241,3 @@ public enum Wrapping: String { /// The corresponding CSS class name for this wrapping behavior. var className: String { "text-\(rawValue)" } } - -extension Element { - /// Applies font styling to the element with optional modifiers. - /// - /// This comprehensive method allows controlling all aspects of typography including - /// size, weight, alignment, spacing, color, and font family. Each parameter targets - /// a specific aspect of text appearance, and can be combined with modifiers for - /// responsive or state-based typography. - /// - /// - Parameters: - /// - size: The font size from extra-small to extra-large variants. - /// - weight: The font weight from thin to black/heavy. - /// - alignment: The text alignment (left, center, right). - /// - tracking: The letter spacing (character spacing). - /// - leading: The line height (vertical spacing between lines). - /// - decoration: The text decoration style (underline, strikethrough, etc.). - /// - wrapping: The text wrapping behavior. - /// - color: The text color from the color palette. - /// - family: The font family name or stack (e.g., "sans-serif"). - /// - modifiers: Zero or more modifiers to scope the styles (e.g., responsive breakpoints or states). - /// - Returns: A new element with updated font styling classes. - /// - /// ## Example - /// ```swift - /// Text { "Welcome to our site" } - /// .font( - /// size: .xl2, - /// weight: .bold, - /// alignment: .center, - /// tracking: .wide, - /// color: .blue(._600) - /// ) - /// - /// // Responsive typography - /// Heading(.one) { "Responsive Title" } - /// .font(size: .xl3) - /// .font(size: .xl5, on: .lg) // Larger on desktop - /// ``` - public func font( - size: TextSize? = nil, - weight: Weight? = nil, - alignment: Alignment? = nil, - tracking: Tracking? = nil, - leading: Leading? = nil, - decoration: Decoration? = nil, - wrapping: Wrapping? = nil, - color: Color? = nil, - family: String? = nil, - on modifiers: Modifier... - ) -> Element { - var baseClasses: [String] = [] - if let size = size { baseClasses.append(size.className) } - if let weight = weight { baseClasses.append(weight.className) } - if let alignment = alignment { baseClasses.append(alignment.className) } - if let tracking = tracking { baseClasses.append(tracking.className) } - if let leading = leading { baseClasses.append(leading.className) } - if let decoration = decoration { baseClasses.append(decoration.className) } - if let wrapping = wrapping { baseClasses.append(wrapping.className) } - if let color = color { baseClasses.append("text-\(color.rawValue)") } - if let family = family { baseClasses.append("font-[\(family)]") } - - let newClasses = combineClasses(baseClasses, withModifiers: modifiers) - - return Element( - tag: self.tag, - id: self.id, - classes: (self.classes ?? []) + newClasses, - role: self.role, - label: self.label, - isSelfClosing: self.isSelfClosing, - customAttributes: self.customAttributes, - content: self.contentBuilder - ) - } - - /// Prose size variants for article/content formatting. - /// - /// Defines standardized sizes for rich text content styling, useful for - /// blog posts, articles, or other long-form content sections. - /// - /// ## Example - /// ```swift - /// Article() - /// .font(proseSize: .lg) - /// ``` - public enum ProseSize: String { - /// Small prose size for compact content layouts. - case sm - - /// Default prose size for standard content. - case base - - /// Large prose size for improved readability. - case lg - - /// Extra large prose size for featured content. - case xl - - /// Double extra large prose size for prominent content. - case xl2 = "2xl" - - /// The corresponding Tailwind CSS class value. - public var rawValue: String { - "prose-\(self)" - } - } - - /// Prose color themes for long-form content. - /// - /// Defines color palettes for content styling, affecting headings, - /// links, and other elements within long-form content. - /// - /// ## Example - /// ```swift - /// Article() - /// .font(proseColor: .slate) - /// ``` - public enum ProseColor: String { - /// Gray color theme for content. - case gray - - /// Slate color theme for content (blueish gray). - case slate - - /// Zinc color theme for content (neutral gray). - case zinc - - /// Neutral color theme for content (balanced gray). - case neutral - - /// The corresponding Tailwind CSS class value. - public var rawValue: String { - "prose-\(self)" - } - } -} diff --git a/Sources/WebUI/Styles/Base/MarginsStyleOperation.swift b/Sources/WebUI/Styles/Base/MarginsStyleOperation.swift new file mode 100644 index 00000000..5ff69c78 --- /dev/null +++ b/Sources/WebUI/Styles/Base/MarginsStyleOperation.swift @@ -0,0 +1,125 @@ +import Foundation + +/// Style operation for margin styling +/// +/// Provides a unified implementation for margin styling that can be used across +/// Element methods and the Declaritive DSL functions. +public struct MarginsStyleOperation: StyleOperation, @unchecked Sendable { + /// Parameters for margin styling + public struct Parameters { + /// The margin value + public let length: Int? + + /// The edges to apply the margin to + public let edges: [Edge] + + /// Whether to use automatic margins + public let auto: Bool + + /// Creates parameters for margin styling + /// + /// - Parameters: + /// - length: The margin value + /// - edges: The edges to apply the margin to + /// - auto: Whether to use automatic margins + public init( + length: Int? = 4, + edges: [Edge] = [.all], + auto: Bool = false + ) { + self.length = length + self.edges = edges.isEmpty ? [.all] : edges + self.auto = auto + } + + /// Creates parameters from a StyleParameters container + /// + /// - Parameter params: The style parameters container + /// - Returns: Parameters object for margin styling + public static func from(_ params: StyleParameters) -> Parameters { + Parameters( + length: params.get("length"), + edges: params.get("edges", default: [.all]), + auto: params.get("auto", default: false) + ) + } + } + + /// Applies the margin style and returns the appropriate CSS classes + /// + /// - Parameter params: The parameters for margin styling + /// - Returns: An array of CSS class names to be applied + public func applyClasses(params: Parameters) -> [String] { + var classes: [String] = [] + + for edge in params.edges { + let prefix = edge == .all ? "m" : "m\(edge.rawValue)" + + if params.auto { + classes.append("\(prefix)-auto") + } else if let length = params.length { + classes.append("\(prefix)-\(length)") + } + } + + return classes + } + + /// Shared instance for use across the framework + public static let shared = MarginsStyleOperation() + + /// Private initializer to enforce singleton usage + private init() {} +} + +// Extension for Element to provide margin styling +extension Element { + /// Applies margin styling to the element with one or more edges. + /// + /// - Parameters: + /// - length: The spacing value in `0.25rem` increments. + /// - edges: One or more edges to apply the margin to. Defaults to `.all`. + /// - auto: Whether to use automatic margins instead of a specific length. + /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. + /// - Returns: A new element with updated margin classes. + public func margins( + of length: Int? = 4, + at edges: Edge..., + auto: Bool = false, + on modifiers: Modifier... + ) -> Element { + let params = MarginsStyleOperation.Parameters( + length: length, + edges: edges, + auto: auto + ) + + return MarginsStyleOperation.shared.applyToElement( + self, + params: params, + modifiers: modifiers + ) + } +} + +// Global function for Declaritive DSL +/// Applies margin styling in the responsive context with Declaritive syntax. +/// +/// - Parameters: +/// - length: The margin value. +/// - edges: The edges to apply margin to. +/// - auto: Whether to use automatic margins. +/// - Returns: A responsive modification for margins. +public func margins( + of length: Int? = 4, + at edges: Edge..., + auto: Bool = false +) -> ResponsiveModification { + let params = MarginsStyleOperation.Parameters( + length: length, + edges: edges, + auto: auto + ) + + return MarginsStyleOperation.shared.asModification(params: params) +} diff --git a/Sources/WebUI/Styles/Base/PaddingStyleOperation.swift b/Sources/WebUI/Styles/Base/PaddingStyleOperation.swift new file mode 100644 index 00000000..4311c6eb --- /dev/null +++ b/Sources/WebUI/Styles/Base/PaddingStyleOperation.swift @@ -0,0 +1,111 @@ +import Foundation + +/// Style operation for padding styling +/// +/// Provides a unified implementation for padding styling that can be used across +/// Element methods and the Declaritive DSL functions. +public struct PaddingStyleOperation: StyleOperation, @unchecked Sendable { + /// Parameters for padding styling + public struct Parameters { + /// The padding value + public let length: Int? + + /// The edges to apply the padding to + public let edges: [Edge] + + /// Creates parameters for padding styling + /// + /// - Parameters: + /// - length: The padding value + /// - edges: The edges to apply the padding to + public init( + length: Int? = 4, + edges: [Edge] = [.all] + ) { + self.length = length + self.edges = edges.isEmpty ? [.all] : edges + } + + /// Creates parameters from a StyleParameters container + /// + /// - Parameter params: The style parameters container + /// - Returns: PaddingStyleOperation.Parameters + public static func from(_ params: StyleParameters) -> Parameters { + Parameters( + length: params.get("length"), + edges: params.get("edges", default: [.all]) + ) + } + } + + /// Applies the padding style and returns the appropriate CSS classes + /// + /// - Parameter params: The parameters for padding styling + /// - Returns: An array of CSS class names to be applied + public func applyClasses(params: Parameters) -> [String] { + var classes: [String] = [] + + guard let length = params.length else { + return classes + } + + for edge in params.edges { + let prefix = edge == .all ? "p" : "p\(edge.rawValue)" + classes.append("\(prefix)-\(length)") + } + + return classes + } + + /// Shared instance for use across the framework + public static let shared = PaddingStyleOperation() + + /// Private initializer to enforce singleton usage + private init() {} +} + +// Extension for Element to provide padding styling +extension Element { + /// Applies padding styling to the element with one or more edges. + /// + /// - Parameters: + /// - length: The spacing value in `0.25rem` increments. + /// - edges: One or more edges to apply the padding to. Defaults to `.all`. + /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. + /// - Returns: A new element with updated padding classes. + public func padding( + of length: Int? = 4, + at edges: Edge..., + on modifiers: Modifier... + ) -> Element { + let params = PaddingStyleOperation.Parameters( + length: length, + edges: edges + ) + + return PaddingStyleOperation.shared.applyToElement( + self, + params: params, + modifiers: modifiers + ) + } +} + +// Global function for Declaritive DSL +/// Applies padding styling in the responsive context with Declaritive syntax. +/// +/// - Parameters: +/// - length: The padding value. +/// - edges: The edges to apply padding to. +/// - Returns: A responsive modification for padding. +public func padding( + of length: Int? = 4, + at edges: Edge... +) -> ResponsiveModification { + let params = PaddingStyleOperation.Parameters( + length: length, + edges: edges + ) + + return PaddingStyleOperation.shared.asModification(params: params) +} diff --git a/Sources/WebUI/Styles/Base/Responsive.swift b/Sources/WebUI/Styles/Base/Responsive.swift deleted file mode 100644 index a07dad82..00000000 --- a/Sources/WebUI/Styles/Base/Responsive.swift +++ /dev/null @@ -1,407 +0,0 @@ -import Foundation - -/// Provides a block-based responsive design API for WebUI elements. -/// -/// The responsive modifier allows developers to define responsive styles -/// across different breakpoints in a single, concise block, creating cleaner -/// and more maintainable code for responsive designs. -/// -/// ## Example -/// ```swift -/// Text { "Responsive Content" } -/// .font(size: .sm) -/// .background(color: .neutral(._500)) -/// .responsive { -/// $0.md { -/// $0.font(size: .lg) -/// $0.background(color: .neutral(._700)) -/// $0.padding(of: 4) -/// } -/// $0.lg { -/// $0.font(size: .xl) -/// $0.background(color: .neutral(._900)) -/// $0.font(alignment: .center) -/// } -/// } -/// ``` -extension Element { - /// Applies responsive styling across different breakpoints. - /// - /// This method provides a clean, declarative way to define styles for multiple - /// breakpoints in a single block, improving code readability and maintainability - /// compared to chaining multiple individual responsive modifiers. - /// - /// The configuration closure accepts a `ResponsiveBuilder` that provides methods - /// for each breakpoint (e.g., `.md`, `.lg`). Within each breakpoint block, you can - /// apply multiple style modifications that will only be applied at that breakpoint. - /// - /// - Parameter configuration: A closure defining responsive style configurations. - /// - Returns: An element with responsive styles applied. - /// - /// ## Example - /// ```swift - /// Button { "Submit" } - /// .background(color: .blue(._500)) - /// .responsive { - /// $0.sm { - /// $0.padding(of: 2) - /// $0.font(size: .sm) - /// } - /// $0.md { - /// $0.padding(of: 4) - /// $0.font(size: .base) - /// } - /// $0.lg { - /// $0.padding(of: 6) - /// $0.font(size: .lg) - /// } - /// } - /// ``` - public func responsive(_ configuration: (ResponsiveBuilder) -> Void) -> Element { - let builder = ResponsiveBuilder(element: self) - configuration(builder) - return builder.element - } -} - -/// Builds responsive style configurations for elements across different breakpoints. -/// -/// `ResponsiveBuilder` provides a fluent, method-chaining API for applying style -/// modifications at specific screen sizes. Each method represents a breakpoint -/// and accepts a closure where style modifications can be defined. -/// -/// This class is not typically created directly, but instead used through the -/// `Element.responsive(_:)` method. -public class ResponsiveBuilder { - /// The current element being modified - var element: Element - /// Keep track of responsive styles for each breakpoint - private var pendingClasses: [String] = [] - /// The current breakpoint being modified - private var currentBreakpoint: Modifier? - - /// Creates a new responsive builder for the given element. - /// - /// - Parameter element: The element to apply responsive styles to. - init(element: Element) { - self.element = element - } - - /// Applies styles at the extra-small breakpoint (480px+). - /// - /// - Parameter modifications: A closure containing style modifications. - /// - Returns: The builder for method chaining. - @discardableResult - public func xs(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { - currentBreakpoint = .xs - modifications(self) - applyBreakpoint() - return self - } - - /// Applies styles at the small breakpoint (640px+). - /// - /// - Parameter modifications: A closure containing style modifications. - /// - Returns: The builder for method chaining. - @discardableResult - public func sm(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { - currentBreakpoint = .sm - modifications(self) - applyBreakpoint() - return self - } - - /// Applies styles at the medium breakpoint (768px+). - /// - /// - Parameter modifications: A closure containing style modifications. - /// - Returns: The builder for method chaining. - @discardableResult - public func md(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { - currentBreakpoint = .md - modifications(self) - applyBreakpoint() - return self - } - - /// Applies styles at the large breakpoint (1024px+). - /// - /// - Parameter modifications: A closure containing style modifications. - /// - Returns: The builder for method chaining. - @discardableResult - public func lg(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { - currentBreakpoint = .lg - modifications(self) - applyBreakpoint() - return self - } - - /// Applies styles at the extra-large breakpoint (1280px+). - /// - /// - Parameter modifications: A closure containing style modifications. - /// - Returns: The builder for method chaining. - @discardableResult - public func xl(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { - currentBreakpoint = .xl - modifications(self) - applyBreakpoint() - return self - } - - /// Applies styles at the 2x-extra-large breakpoint (1536px+). - /// - /// - Parameter modifications: A closure containing style modifications. - /// - Returns: The builder for method chaining. - @discardableResult - public func xl2(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { - currentBreakpoint = .xl2 - modifications(self) - applyBreakpoint() - return self - } - - /// Applies the breakpoint prefix to all pending classes and add them to the element - private func applyBreakpoint() { - guard let breakpoint = currentBreakpoint else { return } - - // Apply the breakpoint prefix to all pending classes - let responsiveClasses = pendingClasses.map { - // Handle duplication for flex-*, justify-*, items-* - if $0.starts(with: "flex-") || $0.starts(with: "justify-") || $0.starts(with: "items-") - || $0.starts(with: "grid-") - { - return "\(breakpoint.rawValue)\($0)" - } else if $0 == "flex" || $0 == "grid" { - return "\(breakpoint.rawValue)\($0)" - } else { - return "\(breakpoint.rawValue)\($0)" - } - } - - // Add the responsive classes to the element - self.element = Element( - tag: self.element.tag, - id: self.element.id, - classes: (self.element.classes ?? []) + responsiveClasses, - role: self.element.role, - label: self.element.label, - data: self.element.data, - isSelfClosing: self.element.isSelfClosing, - customAttributes: self.element.customAttributes, - content: self.element.contentBuilder - ) - - // Clear pending classes for the next breakpoint - pendingClasses = [] - currentBreakpoint = nil - } - - /// Add a class to the pending list of classes - private func addClass(_ className: String) { - pendingClasses.append(className) - } -} - -// Font styling methods -extension ResponsiveBuilder { - @discardableResult - public func font( - size: TextSize? = nil, - weight: Weight? = nil, - alignment: Alignment? = nil, - tracking: Tracking? = nil, - leading: Leading? = nil, - decoration: Decoration? = nil, - wrapping: Wrapping? = nil, - color: Color? = nil, - family: String? = nil - ) -> ResponsiveBuilder { - if let size = size { addClass(size.className) } - if let weight = weight { addClass(weight.className) } - if let alignment = alignment { addClass(alignment.className) } - if let tracking = tracking { addClass(tracking.className) } - if let leading = leading { addClass(leading.className) } - if let decoration = decoration { addClass(decoration.className) } - if let wrapping = wrapping { addClass(wrapping.className) } - if let color = color { addClass("text-\(color.rawValue)") } - if let family = family { addClass("font-[\(family)]") } - return self - } - - @discardableResult - public func background(color: Color) -> ResponsiveBuilder { - addClass("bg-\(color.rawValue)") - return self - } - - @discardableResult - public func padding(of length: Int? = 4, at edges: Edge...) -> ResponsiveBuilder { - let edgesList = edges.isEmpty ? [Edge.all] : edges - for edge in edgesList { - guard let length = length else { continue } - let prefix = edge == .all ? "p" : "p\(edge.rawValue)" - addClass("\(prefix)-\(length)") - } - return self - } - - @discardableResult - public func margins(of length: Int? = 4, at edges: Edge..., auto: Bool = false) -> ResponsiveBuilder { - let edgesList = edges.isEmpty ? [Edge.all] : edges - for edge in edgesList { - let prefix = edge == .all ? "m" : "m\(edge.rawValue)" - if auto { - addClass("\(prefix)-auto") - } else if let length = length { - addClass("\(prefix)-\(length)") - } - } - return self - } - - @discardableResult - public func border( - of width: Int? = 1, - at edges: Edge..., - style: BorderStyle? = nil, - color: Color? = nil - ) -> ResponsiveBuilder { - let edgesList = edges.isEmpty ? [Edge.all] : edges - - for edge in edgesList { - if let style = style, style == .divide { - if let width = width { - let divideClass = edge == .horizontal ? "divide-x-\(width)" : "divide-y-\(width)" - addClass(divideClass) - } - } else { - let prefix = edge == .all ? "border" : "border-\(edge.rawValue)" - if let width = width { - addClass("\(prefix)-\(width)") - } else { - addClass(prefix) - } - } - } - - if let style = style, style != .divide { - addClass("border-\(style.rawValue)") - } - - if let color = color { - addClass("border-\(color.rawValue)") - } - - return self - } - - @discardableResult - public func opacity(_ value: Int) -> ResponsiveBuilder { - addClass("opacity-\(value)") - return self - } - - @discardableResult - public func size(_ value: Int) -> ResponsiveBuilder { - addClass("size-\(value)") - return self - } - - @discardableResult - public func frame( - width: Int? = nil, - height: Int? = nil, - minWidth: Int? = nil, - maxWidth: Int? = nil, - minHeight: Int? = nil, - maxHeight: Int? = nil - ) -> ResponsiveBuilder { - if let width = width { addClass("w-\(width)") } - if let height = height { addClass("h-\(height)") } - if let minWidth = minWidth { addClass("min-w-\(minWidth)") } - if let maxWidth = maxWidth { addClass("max-w-\(maxWidth)") } - if let minHeight = minHeight { addClass("min-h-\(minHeight)") } - if let maxHeight = maxHeight { addClass("max-h-\(maxHeight)") } - return self - } - - @discardableResult - public func flex( - direction: Direction? = nil, - justify: Justify? = nil, - align: Align? = nil, - grow: Grow? = nil - ) -> ResponsiveBuilder { - addClass("flex") - if let direction = direction { addClass("flex-\(direction.rawValue)") } - if let justify = justify { addClass("justify-\(justify.rawValue)") } - if let align = align { addClass("items-\(align.rawValue)") } - if let grow = grow { addClass("flex-\(grow.rawValue)") } - return self - } - - @discardableResult - public func grid( - columns: Int? = nil, - rows: Int? = nil, - justify: Justify? = nil, - align: Align? = nil - ) -> ResponsiveBuilder { - if columns == nil && rows == nil && justify == nil && align == nil { - addClass("grid") - } else { - if let columns = columns { addClass("grid-cols-\(columns)") } - if let rows = rows { addClass("grid-rows-\(rows)") } - if let justify = justify { addClass("justify-\(justify.rawValue)") } - if let align = align { addClass("items-\(align.rawValue)") } - } - return self - } - - @discardableResult - public func position(_ type: PositionType? = nil, at edges: Edge..., offset: Int? = nil) -> ResponsiveBuilder { - if let type = type { - addClass(type.rawValue) - } - - for edge in edges { - if let offset = offset { - let prefix = edge == .horizontal ? "inset-x" : (edge == .vertical ? "inset-y" : edge.rawValue) - let className = - prefix == "t" - ? "top-\(offset)" - : prefix == "b" - ? "bottom-\(offset)" - : prefix == "l" ? "left-\(offset)" : prefix == "r" ? "right-\(offset)" : "\(prefix)-\(offset)" - addClass(className) - } - } - return self - } - - @discardableResult - public func overflow(_ type: OverflowType, axis: Axis = .both) -> ResponsiveBuilder { - let className = axis == .both ? "overflow-\(type.rawValue)" : "overflow-\(axis.rawValue)-\(type.rawValue)" - addClass(className) - return self - } - - @discardableResult - public func hidden(_ isHidden: Bool = true) -> ResponsiveBuilder { - if isHidden { - addClass("hidden") - } - return self - } - - @discardableResult - public func rounded(_ size: RadiusSize? = .md, _ sides: RadiusSide...) -> ResponsiveBuilder { - if sides.isEmpty { - addClass("rounded\(size != nil ? "-\(size!.rawValue)" : "")") - } else { - for side in sides { - addClass("rounded-\(side.rawValue)\(size != nil ? "-\(size!.rawValue)" : "")") - } - } - return self - } -} diff --git a/Sources/WebUI/Styles/Base/Sizing.swift b/Sources/WebUI/Styles/Base/Sizing.swift deleted file mode 100644 index f1578091..00000000 --- a/Sources/WebUI/Styles/Base/Sizing.swift +++ /dev/null @@ -1,344 +0,0 @@ -import CoreGraphics - -/// Represents sizing options for elements. -public enum SizingValue: Sendable { - /// Fixed size in spacing units (e.g., w-4) - case spacing(Int) - /// Fractional size (e.g., w-1/2) - case fraction(Int, Int) - /// Container width presets - case container(ContainerSize) - /// Special viewport units - case viewport(ViewportUnit) - /// Predefined size constants - case constant(SizeConstant) - /// Content-based sizing - case content(ContentSize) - /// Character-based width (e.g., 60ch) - case character(Int) - /// Custom CSS value - case custom(String) - - /// Represents container size presets available in Tailwind CSS - public enum ContainerSize: String, Sendable { - case threeExtraSmall = "3xs" - case twoExtraSmall = "2xs" - case extraSmall = "xs" - case small = "sm" - case medium = "md" - case large = "lg" - case extraLarge = "xl" - case twoExtraLarge = "2xl" - case threeExtraLarge = "3xl" - case fourExtraLarge = "4xl" - case fiveExtraLarge = "5xl" - case sixExtraLarge = "6xl" - case sevenExtraLarge = "7xl" - } - - /// Represents viewport sizing units available in Tailwind CSS - public enum ViewportUnit: String, Sendable { - case viewWidth = "screen" - case dynamicViewWidth = "dvw" - case largeViewWidth = "lvw" - case smallViewWidth = "svw" - case dynamicViewHeight = "dvh" - case largeViewHeight = "lvh" - case smallViewHeight = "svh" - } - - /// Represents constant size values available in Tailwind CSS - public enum SizeConstant: String, Sendable { - case auto = "auto" - case px = "px" - case full = "full" - } - - /// Represents content-based sizing available in Tailwind CSS - public enum ContentSize: String, Sendable { - case min = "min" - case max = "max" - case fit = "fit" - } - - public var rawValue: String { - switch self { - case .spacing(let value): - return "\(value)" - case .fraction(let numerator, let denominator): - return "\(numerator)/\(denominator)" - case .container(let size): - return size.rawValue - case .viewport(let unit): - return unit.rawValue - case .constant(let constant): - return constant.rawValue - case .content(let content): - return content.rawValue - case .character(let value): - return "[\(value)ch]" - case .custom(let value): - return "[\(value)]" - } - } -} - -extension Element { - /// Sets the width and height of the element with comprehensive SwiftUI-like API. - /// - /// This method provides control over all width and height properties, supporting - /// all Tailwind CSS sizing options including numeric values, fractions, container sizes, - /// viewport units, constants, content-based sizing, and custom values. - /// - /// - Parameters: - /// - width: The width value. - /// - height: The height value. - /// - minWidth: The minimum width value. - /// - maxWidth: The maximum width value. - /// - minHeight: The minimum height value. - /// - maxHeight: The maximum height value. - /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. - /// - Returns: A new element with updated sizing classes. - public func frame( - width: SizingValue? = nil, - height: SizingValue? = nil, - minWidth: SizingValue? = nil, - maxWidth: SizingValue? = nil, - minHeight: SizingValue? = nil, - maxHeight: SizingValue? = nil, - on modifiers: Modifier... - ) -> Element { - var baseClasses: [String] = [] - - if let width = width { baseClasses.append("w-\(width.rawValue)") } - if let height = height { baseClasses.append("h-\(height.rawValue)") } - if let minWidth = minWidth { baseClasses.append("min-w-\(minWidth.rawValue)") } - if let maxWidth = maxWidth { baseClasses.append("max-w-\(maxWidth.rawValue)") } - if let minHeight = minHeight { baseClasses.append("min-h-\(minHeight.rawValue)") } - if let maxHeight = maxHeight { baseClasses.append("max-h-\(maxHeight.rawValue)") } - - let newClasses = combineClasses(baseClasses, withModifiers: modifiers) - - return Element( - tag: self.tag, - id: self.id, - classes: (self.classes ?? []) + newClasses, - role: self.role, - label: self.label, - isSelfClosing: self.isSelfClosing, - customAttributes: self.customAttributes, - content: self.contentBuilder - ) - } - - /// Sets both width and height to the same value, creating a square element. - /// - /// This method applies the same sizing value to both width and height dimensions. - /// - /// - Parameters: - /// - size: The size value to apply to both width and height. - /// - modifiers: Zero or more modifiers to scope the styles. - /// - Returns: A new element with updated sizing classes. - public func size(_ size: SizingValue, on modifiers: Modifier...) -> Element { - var baseClasses: [String] = [] - baseClasses.append("size-\(size.rawValue)") - - let newClasses = combineClasses(baseClasses, withModifiers: modifiers) - - return Element( - tag: self.tag, - id: self.id, - classes: (self.classes ?? []) + newClasses, - role: self.role, - label: self.label, - isSelfClosing: self.isSelfClosing, - customAttributes: self.customAttributes, - content: self.contentBuilder - ) - } - - /// Sets the width and height of the element using numeric values. - /// - /// This method provides a simplified SwiftUI-compatible signature for setting frame dimensions. - /// - /// - Parameters: - /// - width: The width in spacing units. - /// - height: The height in spacing units. - /// - minWidth: The minimum width in spacing units. - /// - maxWidth: The maximum width in spacing units. - /// - minHeight: The minimum height in spacing units. - /// - maxHeight: The maximum height in spacing units. - /// - Returns: A new element with updated sizing classes. - public func frame( - width: CGFloat? = nil, - height: CGFloat? = nil, - minWidth: CGFloat? = nil, - maxWidth: CGFloat? = nil, - minHeight: CGFloat? = nil, - maxHeight: CGFloat? = nil - ) -> Element { - frame( - width: width.map { .spacing(Int($0)) }, - height: height.map { .spacing(Int($0)) }, - minWidth: minWidth.map { .spacing(Int($0)) }, - maxWidth: maxWidth.map { .spacing(Int($0)) }, - minHeight: minHeight.map { .spacing(Int($0)) }, - maxHeight: maxHeight.map { .spacing(Int($0)) } - ) - } - - /// Creates a frame that maintains the specified aspect ratio. - /// - /// Follows the SwiftUI pattern for creating frames with a fixed aspect ratio. - /// - /// - Parameters: - /// - width: The width for the aspect ratio calculation. - /// - height: The height for the aspect ratio calculation. - /// - modifiers: Zero or more modifiers to scope the styles. - /// - Returns: A new element with aspect ratio classes. - public func aspectRatio(_ width: CGFloat, _ height: CGFloat, on modifiers: Modifier...) -> Element { - let ratio = width / height - let baseClass = "aspect-[\(ratio)]" - let newClasses = combineClasses([baseClass], withModifiers: modifiers) - - return Element( - tag: self.tag, - id: self.id, - classes: (self.classes ?? []) + newClasses, - role: self.role, - label: self.label, - isSelfClosing: self.isSelfClosing, - customAttributes: self.customAttributes, - content: self.contentBuilder - ) - } - - /// Sets the aspect ratio to square (1:1). - /// - /// - Parameters: - /// - modifiers: Zero or more modifiers to scope the styles. - /// - Returns: A new element with square aspect ratio. - public func aspectRatio(on modifiers: Modifier...) -> Element { - let baseClass = "aspect-square" - let newClasses = combineClasses([baseClass], withModifiers: modifiers) - - return Element( - tag: self.tag, - id: self.id, - classes: (self.classes ?? []) + newClasses, - role: self.role, - label: self.label, - isSelfClosing: self.isSelfClosing, - customAttributes: self.customAttributes, - content: self.contentBuilder - ) - } - - /// Sets the aspect ratio to video dimensions (16:9). - /// - /// - Parameters: - /// - modifiers: Zero or more modifiers to scope the styles. - /// - Returns: A new element with video aspect ratio. - public func aspectRatioVideo(on modifiers: Modifier...) -> Element { - let baseClass = "aspect-video" - let newClasses = combineClasses([baseClass], withModifiers: modifiers) - - return Element( - tag: self.tag, - id: self.id, - classes: (self.classes ?? []) + newClasses, - role: self.role, - label: self.label, - isSelfClosing: self.isSelfClosing, - customAttributes: self.customAttributes, - content: self.contentBuilder - ) - } -} - -// MARK: - Convenience Extensions for Improved SwiftUI-like Experience - -extension CGFloat { - /// Converts the CGFloat to a fixed spacing SizingValue. - public var spacing: SizingValue { - .spacing(Int(self)) - } -} - -extension Int { - /// Converts the Int to a fixed spacing SizingValue. - public var spacing: SizingValue { - .spacing(self) - } - - /// Creates a fraction SizingValue with this int as the numerator. - /// - /// - Parameter denominator: The denominator of the fraction. - /// - Returns: A SizingValue representing the fraction. - public func fraction(_ denominator: Int) -> SizingValue { - .fraction(self, denominator) - } - - /// Creates a character width SizingValue. - public var ch: SizingValue { - .character(self) - } -} - -// MARK: - Static Sizing Constants for SwiftUI-like API - -extension SizingValue { - /// Width/height set to auto - public static let auto: SizingValue = .constant(.auto) - - /// Width/height set to 1px - public static let px: SizingValue = .constant(.px) - - /// Width/height set to 100% - public static let full: SizingValue = .constant(.full) - - /// Width/height set to screen viewport - public static let screen: SizingValue = .viewport(.viewWidth) - - /// Width/height set to dynamic viewport width - public static let dvw: SizingValue = .viewport(.dynamicViewWidth) - - /// Width/height set to dynamic viewport height - public static let dvh: SizingValue = .viewport(.dynamicViewHeight) - - /// Width/height set to large viewport width - public static let lvw: SizingValue = .viewport(.largeViewWidth) - - /// Width/height set to large viewport height - public static let lvh: SizingValue = .viewport(.largeViewHeight) - - /// Width/height set to small viewport width - public static let svw: SizingValue = .viewport(.smallViewWidth) - - /// Width/height set to small viewport height - public static let svh: SizingValue = .viewport(.smallViewHeight) - - /// Width/height set to min-content - public static let min: SizingValue = .content(.min) - - /// Width/height set to max-content - public static let max: SizingValue = .content(.max) - - /// Width/height set to fit-content - public static let fit: SizingValue = .content(.fit) - - // Container size presets - public static let xs3: SizingValue = .container(.threeExtraSmall) - public static let xs2: SizingValue = .container(.twoExtraSmall) - public static let xs: SizingValue = .container(.extraSmall) - public static let sm: SizingValue = .container(.small) - public static let md: SizingValue = .container(.medium) - public static let lg: SizingValue = .container(.large) - public static let xl: SizingValue = .container(.extraLarge) - public static let xl2: SizingValue = .container(.twoExtraLarge) - public static let xl3: SizingValue = .container(.threeExtraLarge) - public static let xl4: SizingValue = .container(.fourExtraLarge) - public static let xl5: SizingValue = .container(.fiveExtraLarge) - public static let xl6: SizingValue = .container(.sixExtraLarge) - public static let xl7: SizingValue = .container(.sevenExtraLarge) -} diff --git a/Sources/WebUI/Styles/Base/Sizing/SizingStyleOperation.swift b/Sources/WebUI/Styles/Base/Sizing/SizingStyleOperation.swift new file mode 100644 index 00000000..54b9c2f8 --- /dev/null +++ b/Sources/WebUI/Styles/Base/Sizing/SizingStyleOperation.swift @@ -0,0 +1,546 @@ +import CoreGraphics +import Foundation + +/// Style operation for sizing elements +/// +/// Provides a unified implementation for element sizing that can be used across +/// Element methods and the Declarative DSL functions. +public struct SizingStyleOperation: StyleOperation, @unchecked Sendable { + /// Default parameter type used for the StyleOperation protocol conformance + public typealias Parameters = FrameParameters + /// Parameters for frame styling + public struct FrameParameters { + /// The width value + public let width: SizingValue? + + /// The height value + public let height: SizingValue? + + /// The minimum width value + public let minWidth: SizingValue? + + /// The maximum width value + public let maxWidth: SizingValue? + + /// The minimum height value + public let minHeight: SizingValue? + + /// The maximum height value + public let maxHeight: SizingValue? + + /// Creates parameters for frame styling + /// + /// - Parameters: + /// - width: The width value + /// - height: The height value + /// - minWidth: The minimum width value + /// - maxWidth: The maximum width value + /// - minHeight: The minimum height value + /// - maxHeight: The maximum height value + public init( + width: SizingValue? = nil, + height: SizingValue? = nil, + minWidth: SizingValue? = nil, + maxWidth: SizingValue? = nil, + minHeight: SizingValue? = nil, + maxHeight: SizingValue? = nil + ) { + self.width = width + self.height = height + self.minWidth = minWidth + self.maxWidth = maxWidth + self.minHeight = minHeight + self.maxHeight = maxHeight + } + + /// Creates parameters from a StyleParameters container + /// + /// - Parameter params: The style parameters container + /// - Returns: SizingStyleOperation.FrameParameters + public static func from(_ params: StyleParameters) -> FrameParameters { + FrameParameters( + width: params.get("width"), + height: params.get("height"), + minWidth: params.get("minWidth"), + maxWidth: params.get("maxWidth"), + minHeight: params.get("minHeight"), + maxHeight: params.get("maxHeight") + ) + } + } + + /// Parameters for size styling (uniform sizing) + public struct SizeParameters { + /// The size value to apply to both width and height + public let value: SizingValue + + /// Creates parameters for size styling + /// + /// - Parameter value: The size value + public init(value: SizingValue) { + self.value = value + } + + /// Creates parameters from a StyleParameters container + /// + /// - Parameter params: The style parameters container + /// - Returns: SizingStyleOperation.SizeParameters + public static func from(_ params: StyleParameters) -> SizeParameters { + SizeParameters( + value: params.get("value")! + ) + } + } + + /// Parameters for aspect ratio styling + public struct AspectRatioParameters { + /// The width component of the aspect ratio + public let width: CGFloat? + + /// The height component of the aspect ratio + public let height: CGFloat? + + /// Whether to use a square (1:1) aspect ratio + public let isSquare: Bool + + /// Whether to use a video (16:9) aspect ratio + public let isVideo: Bool + + /// Creates parameters for aspect ratio styling + /// + /// - Parameters: + /// - width: The width component + /// - height: The height component + /// - isSquare: Whether to use a square aspect ratio + /// - isVideo: Whether to use a video aspect ratio + public init( + width: CGFloat? = nil, + height: CGFloat? = nil, + isSquare: Bool = false, + isVideo: Bool = false + ) { + self.width = width + self.height = height + self.isSquare = isSquare + self.isVideo = isVideo + } + + /// Creates parameters from a StyleParameters container + /// + /// - Parameter params: The style parameters container + /// - Returns: SizingStyleOperation.AspectRatioParameters + public static func from(_ params: StyleParameters) -> AspectRatioParameters { + AspectRatioParameters( + width: params.get("width"), + height: params.get("height"), + isSquare: params.get("isSquare", default: false), + isVideo: params.get("isVideo", default: false) + ) + } + } + + /// Applies the frame style and returns the appropriate CSS classes + /// + /// - Parameter params: The parameters for frame styling + /// - Returns: An array of CSS class names to be applied to elements + public func applyClasses(params: Parameters) -> [String] { + var classes: [String] = [] + + if let width = params.width { classes.append("w-\(width.rawValue)") } + if let height = params.height { classes.append("h-\(height.rawValue)") } + if let minWidth = params.minWidth { classes.append("min-w-\(minWidth.rawValue)") } + if let maxWidth = params.maxWidth { classes.append("max-w-\(maxWidth.rawValue)") } + if let minHeight = params.minHeight { classes.append("min-h-\(minHeight.rawValue)") } + if let maxHeight = params.maxHeight { classes.append("max-h-\(maxHeight.rawValue)") } + + return classes + } + + public func applyFrameClasses(params: FrameParameters) -> [String] { + applyClasses(params: params) + } + + /// Applies the size style and returns the appropriate CSS classes + /// + /// - Parameter params: The parameters for size styling + /// - Returns: An array of CSS class names to be applied to elements + public func applySizeClasses(params: SizeParameters) -> [String] { + ["size-\(params.value.rawValue)"] + } + + /// Applies the aspect ratio style and returns the appropriate CSS classes + /// + /// - Parameter params: The parameters for aspect ratio styling + /// - Returns: An array of CSS class names to be applied to elements + public func applyAspectRatioClasses(params: AspectRatioParameters) -> [String] { + if params.isSquare { + return ["aspect-square"] + } else if params.isVideo { + return ["aspect-video"] + } else if let width = params.width, let height = params.height { + let ratio = width / height + return ["aspect-[\(ratio)]"] + } + return [] + } + + /// Shared instance for use across the framework + public static let shared = SizingStyleOperation() + + /// Private initializer to enforce singleton usage + private init() {} +} + +// Extension for Element to provide frame styling +extension Element { + /// Sets the width and height of the element with comprehensive SwiftUI-like API. + /// + /// This method provides control over all width and height properties, supporting + /// all Tailwind CSS sizing options including numeric values, fractions, container sizes, + /// viewport units, constants, content-based sizing, and custom values. + /// + /// - Parameters: + /// - width: The width value. + /// - height: The height value. + /// - minWidth: The minimum width value. + /// - maxWidth: The maximum width value. + /// - minHeight: The minimum height value. + /// - maxHeight: The maximum height value. + /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. + /// - Returns: A new element with updated sizing classes. + public func frame( + width: SizingValue? = nil, + height: SizingValue? = nil, + minWidth: SizingValue? = nil, + maxWidth: SizingValue? = nil, + minHeight: SizingValue? = nil, + maxHeight: SizingValue? = nil, + on modifiers: Modifier... + ) -> Element { + let params = SizingStyleOperation.FrameParameters( + width: width, + height: height, + minWidth: minWidth, + maxWidth: maxWidth, + minHeight: minHeight, + maxHeight: maxHeight + ) + + let classes = SizingStyleOperation.shared.applyFrameClasses(params: params) + let newClasses = combineClasses(classes, withModifiers: modifiers) + + return Element( + tag: self.tag, + id: self.id, + classes: (self.classes ?? []) + newClasses, + role: self.role, + label: self.label, + isSelfClosing: self.isSelfClosing, + customAttributes: self.customAttributes, + content: self.contentBuilder + ) + } + + /// Sets the width and height of the element using numeric values. + /// + /// This method provides a simplified SwiftUI-compatible signature for setting frame dimensions. + /// + /// - Parameters: + /// - width: The width in spacing units. + /// - height: The height in spacing units. + /// - minWidth: The minimum width in spacing units. + /// - maxWidth: The maximum width in spacing units. + /// - minHeight: The minimum height in spacing units. + /// - maxHeight: The maximum height in spacing units. + /// - Returns: A new element with updated sizing classes. + public func frame( + width: CGFloat? = nil, + height: CGFloat? = nil, + minWidth: CGFloat? = nil, + maxWidth: CGFloat? = nil, + minHeight: CGFloat? = nil, + maxHeight: CGFloat? = nil + ) -> Element { + frame( + width: width.map { .spacing(Int($0)) }, + height: height.map { .spacing(Int($0)) }, + minWidth: minWidth.map { .spacing(Int($0)) }, + maxWidth: maxWidth.map { .spacing(Int($0)) }, + minHeight: minHeight.map { .spacing(Int($0)) }, + maxHeight: maxHeight.map { .spacing(Int($0)) } + ) + } + + /// Sets both width and height to the same value, creating a square element. + /// + /// This method applies the same sizing value to both width and height dimensions. + /// + /// - Parameters: + /// - size: The size value to apply to both width and height. + /// - modifiers: Zero or more modifiers to scope the styles. + /// - Returns: A new element with updated sizing classes. + public func size(_ size: SizingValue, on modifiers: Modifier...) -> Element { + let params = SizingStyleOperation.SizeParameters(value: size) + + let classes = SizingStyleOperation.shared.applySizeClasses(params: params) + let newClasses = combineClasses(classes, withModifiers: modifiers) + + return Element( + tag: self.tag, + id: self.id, + classes: (self.classes ?? []) + newClasses, + role: self.role, + label: self.label, + isSelfClosing: self.isSelfClosing, + customAttributes: self.customAttributes, + content: self.contentBuilder + ) + } + + /// Creates a frame that maintains the specified aspect ratio. + /// + /// Follows the SwiftUI pattern for creating frames with a fixed aspect ratio. + /// + /// - Parameters: + /// - width: The width for the aspect ratio calculation. + /// - height: The height for the aspect ratio calculation. + /// - modifiers: Zero or more modifiers to scope the styles. + /// - Returns: A new element with aspect ratio classes. + public func aspectRatio(_ width: CGFloat, _ height: CGFloat, on modifiers: Modifier...) -> Element { + let params = SizingStyleOperation.AspectRatioParameters(width: width, height: height) + + let classes = SizingStyleOperation.shared.applyAspectRatioClasses(params: params) + let newClasses = combineClasses(classes, withModifiers: modifiers) + + return Element( + tag: self.tag, + id: self.id, + classes: (self.classes ?? []) + newClasses, + role: self.role, + label: self.label, + isSelfClosing: self.isSelfClosing, + customAttributes: self.customAttributes, + content: self.contentBuilder + ) + } + + /// Sets the aspect ratio to square (1:1). + /// + /// - Parameters: + /// - modifiers: Zero or more modifiers to scope the styles. + /// - Returns: A new element with square aspect ratio. + public func aspectRatio(on modifiers: Modifier...) -> Element { + let params = SizingStyleOperation.AspectRatioParameters(isSquare: true) + + let classes = SizingStyleOperation.shared.applyAspectRatioClasses(params: params) + let newClasses = combineClasses(classes, withModifiers: modifiers) + + return Element( + tag: self.tag, + id: self.id, + classes: (self.classes ?? []) + newClasses, + role: self.role, + label: self.label, + isSelfClosing: self.isSelfClosing, + customAttributes: self.customAttributes, + content: self.contentBuilder + ) + } + + /// Sets the aspect ratio to video dimensions (16:9). + /// + /// - Parameters: + /// - modifiers: Zero or more modifiers to scope the styles. + /// - Returns: A new element with video aspect ratio. + public func aspectRatioVideo(on modifiers: Modifier...) -> Element { + let params = SizingStyleOperation.AspectRatioParameters(isVideo: true) + + let classes = SizingStyleOperation.shared.applyAspectRatioClasses(params: params) + let newClasses = combineClasses(classes, withModifiers: modifiers) + + return Element( + tag: self.tag, + id: self.id, + classes: (self.classes ?? []) + newClasses, + role: self.role, + label: self.label, + isSelfClosing: self.isSelfClosing, + customAttributes: self.customAttributes, + content: self.contentBuilder + ) + } +} + +// Extension for ResponsiveBuilder to provide sizing styling +extension ResponsiveBuilder { + /// Applies frame styling in a responsive context. + /// + /// - Parameters: + /// - width: The width value. + /// - height: The height value. + /// - minWidth: The minimum width value. + /// - maxWidth: The maximum width value. + /// - minHeight: The minimum height value. + /// - maxHeight: The maximum height value. + /// - Returns: The builder for method chaining. + @discardableResult + public func frame( + width: SizingValue? = nil, + height: SizingValue? = nil, + minWidth: SizingValue? = nil, + maxWidth: SizingValue? = nil, + minHeight: SizingValue? = nil, + maxHeight: SizingValue? = nil + ) -> ResponsiveBuilder { + let params = SizingStyleOperation.FrameParameters( + width: width, + height: height, + minWidth: minWidth, + maxWidth: maxWidth, + minHeight: minHeight, + maxHeight: maxHeight + ) + + let classes = SizingStyleOperation.shared.applyFrameClasses(params: params) + for className in classes { + addClass(className) + } + + return self + } + + /// Applies size styling in a responsive context. + /// + /// - Parameter value: The size value. + /// - Returns: The builder for method chaining. + @discardableResult + public func size(_ value: SizingValue) -> ResponsiveBuilder { + let params = SizingStyleOperation.SizeParameters(value: value) + + let classes = SizingStyleOperation.shared.applySizeClasses(params: params) + for className in classes { + addClass(className) + } + + return self + } + + /// Applies aspect ratio styling in a responsive context. + /// + /// - Parameters: + /// - width: The width for the aspect ratio calculation. + /// - height: The height for the aspect ratio calculation. + /// - Returns: The builder for method chaining. + @discardableResult + public func aspectRatio(_ width: CGFloat, _ height: CGFloat) -> ResponsiveBuilder { + let params = SizingStyleOperation.AspectRatioParameters(width: width, height: height) + + let classes = SizingStyleOperation.shared.applyAspectRatioClasses(params: params) + for className in classes { + addClass(className) + } + + return self + } + + /// Applies square aspect ratio styling in a responsive context. + /// + /// - Returns: The builder for method chaining. + @discardableResult + public func aspectRatio() -> ResponsiveBuilder { + let params = SizingStyleOperation.AspectRatioParameters(isSquare: true) + + let classes = SizingStyleOperation.shared.applyAspectRatioClasses(params: params) + for className in classes { + addClass(className) + } + + return self + } + + /// Applies video aspect ratio styling in a responsive context. + /// + /// - Returns: The builder for method chaining. + @discardableResult + public func aspectRatioVideo() -> ResponsiveBuilder { + let params = SizingStyleOperation.AspectRatioParameters(isVideo: true) + + let classes = SizingStyleOperation.shared.applyAspectRatioClasses(params: params) + for className in classes { + addClass(className) + } + + return self + } +} + +// Global functions for Declarative DSL +/// Applies frame styling in the responsive context. +/// +/// - Parameters: +/// - width: The width value. +/// - height: The height value. +/// - minWidth: The minimum width value. +/// - maxWidth: The maximum width value. +/// - minHeight: The minimum height value. +/// - maxHeight: The maximum height value. +/// - Returns: A responsive modification for frame styling. +public func frame( + width: SizingValue? = nil, + height: SizingValue? = nil, + minWidth: SizingValue? = nil, + maxWidth: SizingValue? = nil, + minHeight: SizingValue? = nil, + maxHeight: SizingValue? = nil +) -> ResponsiveModification { + StyleModification { builder in + _ = builder.frame( + width: width, + height: height, + minWidth: minWidth, + maxWidth: maxWidth, + minHeight: minHeight, + maxHeight: maxHeight + ) + } +} + +/// Applies size styling in the responsive context. +/// +/// - Parameter value: The size value. +/// - Returns: A responsive modification for size styling. +public func size(_ value: SizingValue) -> ResponsiveModification { + StyleModification { builder in + _ = builder.size(value) + } +} + +/// Applies aspect ratio styling in the responsive context. +/// +/// - Parameters: +/// - width: The width for the aspect ratio calculation. +/// - height: The height for the aspect ratio calculation. +/// - Returns: A responsive modification for aspect ratio styling. +public func aspectRatio(_ width: CGFloat, _ height: CGFloat) -> ResponsiveModification { + StyleModification { builder in + _ = builder.aspectRatio(width, height) + } +} + +/// Applies square aspect ratio styling in the responsive context. +/// +/// - Returns: A responsive modification for square aspect ratio styling. +public func aspectRatio() -> ResponsiveModification { + StyleModification { builder in + _ = builder.aspectRatio() + } +} + +/// Applies video aspect ratio styling in the responsive context. +/// +/// - Returns: A responsive modification for video aspect ratio styling. +public func aspectRatioVideo() -> ResponsiveModification { + StyleModification { builder in + _ = builder.aspectRatioVideo() + } +} diff --git a/Sources/WebUI/Styles/Base/Sizing/SizingTypes.swift b/Sources/WebUI/Styles/Base/Sizing/SizingTypes.swift new file mode 100644 index 00000000..5bd9bbb2 --- /dev/null +++ b/Sources/WebUI/Styles/Base/Sizing/SizingTypes.swift @@ -0,0 +1,173 @@ +import CoreGraphics + +/// Represents sizing options for elements. +public enum SizingValue: Sendable { + /// Fixed size in spacing units (e.g., w-4) + case spacing(Int) + /// Fractional size (e.g., w-1/2) + case fraction(Int, Int) + /// Container width presets + case container(ContainerSize) + /// Special viewport units + case viewport(ViewportUnit) + /// Predefined size constants + case constant(SizeConstant) + /// Content-based sizing + case content(ContentSize) + /// Character-based width (e.g., 60ch) + case character(Int) + /// Custom CSS value + case custom(String) + + /// Represents container size presets available in Tailwind CSS + public enum ContainerSize: String, Sendable { + case threeExtraSmall = "3xs" + case twoExtraSmall = "2xs" + case extraSmall = "xs" + case small = "sm" + case medium = "md" + case large = "lg" + case extraLarge = "xl" + case twoExtraLarge = "2xl" + case threeExtraLarge = "3xl" + case fourExtraLarge = "4xl" + case fiveExtraLarge = "5xl" + case sixExtraLarge = "6xl" + case sevenExtraLarge = "7xl" + } + + /// Represents viewport sizing units available in Tailwind CSS + public enum ViewportUnit: String, Sendable { + case viewWidth = "screen" + case dynamicViewWidth = "dvw" + case largeViewWidth = "lvw" + case smallViewWidth = "svw" + case dynamicViewHeight = "dvh" + case largeViewHeight = "lvh" + case smallViewHeight = "svh" + } + + /// Represents constant size values available in Tailwind CSS + public enum SizeConstant: String, Sendable { + case auto = "auto" + case px = "px" + case full = "full" + } + + /// Represents content-based sizing available in Tailwind CSS + public enum ContentSize: String, Sendable { + case min = "min" + case max = "max" + case fit = "fit" + } + + public var rawValue: String { + switch self { + case .spacing(let value): + return "\(value)" + case .fraction(let numerator, let denominator): + return "\(numerator)/\(denominator)" + case .container(let size): + return size.rawValue + case .viewport(let unit): + return unit.rawValue + case .constant(let constant): + return constant.rawValue + case .content(let content): + return content.rawValue + case .character(let value): + return "[\(value)ch]" + case .custom(let value): + return "[\(value)]" + } + } +} + +// Implementation has been moved to SizingStyleOperation.swift + +// MARK: - Convenience Extensions for Improved SwiftUI-like Experience + +extension CGFloat { + /// Converts the CGFloat to a fixed spacing SizingValue. + public var spacing: SizingValue { + .spacing(Int(self)) + } +} + +extension Int { + /// Converts the Int to a fixed spacing SizingValue. + public var spacing: SizingValue { + .spacing(self) + } + + /// Creates a fraction SizingValue with this int as the numerator. + /// + /// - Parameter denominator: The denominator of the fraction. + /// - Returns: A SizingValue representing the fraction. + public func fraction(_ denominator: Int) -> SizingValue { + .fraction(self, denominator) + } + + /// Creates a character width SizingValue. + public var ch: SizingValue { + .character(self) + } +} + +// MARK: - Static Sizing Constants for SwiftUI-like API + +extension SizingValue { + /// Width/height set to auto + public static let auto: SizingValue = .constant(.auto) + + /// Width/height set to 1px + public static let px: SizingValue = .constant(.px) + + /// Width/height set to 100% + public static let full: SizingValue = .constant(.full) + + /// Width/height set to screen viewport + public static let screen: SizingValue = .viewport(.viewWidth) + + /// Width/height set to dynamic viewport width + public static let dvw: SizingValue = .viewport(.dynamicViewWidth) + + /// Width/height set to dynamic viewport height + public static let dvh: SizingValue = .viewport(.dynamicViewHeight) + + /// Width/height set to large viewport width + public static let lvw: SizingValue = .viewport(.largeViewWidth) + + /// Width/height set to large viewport height + public static let lvh: SizingValue = .viewport(.largeViewHeight) + + /// Width/height set to small viewport width + public static let svw: SizingValue = .viewport(.smallViewWidth) + + /// Width/height set to small viewport height + public static let svh: SizingValue = .viewport(.smallViewHeight) + + /// Width/height set to min-content + public static let min: SizingValue = .content(.min) + + /// Width/height set to max-content + public static let max: SizingValue = .content(.max) + + /// Width/height set to fit-content + public static let fit: SizingValue = .content(.fit) + + // Container size presets + public static let xs3: SizingValue = .container(.threeExtraSmall) + public static let xs2: SizingValue = .container(.twoExtraSmall) + public static let xs: SizingValue = .container(.extraSmall) + public static let sm: SizingValue = .container(.small) + public static let md: SizingValue = .container(.medium) + public static let lg: SizingValue = .container(.large) + public static let xl: SizingValue = .container(.extraLarge) + public static let xl2: SizingValue = .container(.twoExtraLarge) + public static let xl3: SizingValue = .container(.threeExtraLarge) + public static let xl4: SizingValue = .container(.fourExtraLarge) + public static let xl5: SizingValue = .container(.fiveExtraLarge) + public static let xl6: SizingValue = .container(.sixExtraLarge) + public static let xl7: SizingValue = .container(.sevenExtraLarge) +} diff --git a/Sources/WebUI/Styles/Base/Spacing.swift b/Sources/WebUI/Styles/Base/Spacing.swift deleted file mode 100644 index 237eabb7..00000000 --- a/Sources/WebUI/Styles/Base/Spacing.swift +++ /dev/null @@ -1,119 +0,0 @@ -extension Element { - /// Applies margin styling to the element with one or more edges. - /// - /// - Parameters: - /// - length: The spacing value in `0.25rem` increments. - /// - edges: One or more edges to apply the margin to. Defaults to `.all`. - /// - auto: Whether to use automatic margins instead of a specific length. - /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. - /// - Returns: A new element with updated margin classes. - public func margins( - of length: Int? = 4, - at edges: Edge..., - auto: Bool = false, - on modifiers: Modifier... - ) -> Element { - let effectiveEdges = edges.isEmpty ? [Edge.all] : edges - let baseClasses: [String] - if auto { - baseClasses = effectiveEdges.map { edge in - let edgeValue = edge.rawValue - return "m\(edgeValue)-auto" - } - } else { - baseClasses = - length.map { lengthValue in - effectiveEdges.map { edge in - let edgeValue = edge.rawValue - return "m\(edgeValue)-\(lengthValue)" - } - } ?? [] - } - - let newClasses = combineClasses(baseClasses, withModifiers: modifiers) - - return Element( - tag: self.tag, - id: self.id, - classes: (self.classes ?? []) + newClasses, - role: self.role, - label: self.label, - isSelfClosing: self.isSelfClosing, - customAttributes: self.customAttributes, - content: self.contentBuilder - ) - } - - /// Applies padding styling to the element with one or more edges. - /// - /// - Parameters: - /// - length: The spacing value. - /// - edges: One or more edges to apply the padding to. Defaults to `.all`. - /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. - /// - Returns: A new element with updated padding classes. - public func padding( - of length: Int? = 4, - at edges: Edge..., - on modifiers: Modifier... - ) -> Element { - let effectiveEdges = edges.isEmpty ? [Edge.all] : edges - let baseClasses: [String] = - length.map { lengthValue in - effectiveEdges.map { edge in - let edgeValue = edge.rawValue - return "p\(edgeValue)-\(lengthValue)" - } - } ?? [] - - let newClasses = combineClasses(baseClasses, withModifiers: modifiers) - - return Element( - tag: self.tag, - id: self.id, - classes: (self.classes ?? []) + newClasses, - role: self.role, - label: self.label, - isSelfClosing: self.isSelfClosing, - customAttributes: self.customAttributes, - content: self.contentBuilder - ) - } - - /// Applies spacing between child elements horizontally and/or vertically. - /// - /// - Parameters: - /// - length: The spacing value in `0.25rem` increments. - /// - direction: The direction(s) to apply spacing (`horizontal`, `vertical`, or both). - /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. - /// - Returns: A new element with updated spacing classes. - public func spacing( - of length: Int? = 4, - along direction: Axis = .both, - on modifiers: Modifier... - ) -> Element { - let baseClasses: [String] = - length.map { lengthValue in - switch direction { - case .x: - return ["space-x-\(lengthValue)"] - case .y: - return ["space-y-\(lengthValue)"] - case .both: - return ["space-x-\(lengthValue)", "space-y-\(lengthValue)"] - } - } ?? [] - - let newClasses = combineClasses(baseClasses, withModifiers: modifiers) - - return Element( - tag: self.tag, - id: self.id, - classes: (self.classes ?? []) + newClasses, - role: self.role, - label: self.label, - isSelfClosing: self.isSelfClosing, - customAttributes: self.customAttributes, - content: self.contentBuilder - ) - } -} diff --git a/Sources/WebUI/Styles/Base/SpacingStyleOperation.swift b/Sources/WebUI/Styles/Base/SpacingStyleOperation.swift new file mode 100644 index 00000000..60ba19f6 --- /dev/null +++ b/Sources/WebUI/Styles/Base/SpacingStyleOperation.swift @@ -0,0 +1,133 @@ +import Foundation + +/// Style operation for spacing styling +/// +/// Provides a unified implementation for spacing styling that can be used across +/// Element methods and the Declarative DSL functions. +public struct SpacingStyleOperation: StyleOperation, @unchecked Sendable { + /// Parameters for spacing styling + public struct Parameters { + /// The spacing value + public let length: Int? + + /// The axis to apply the spacing to + public let axis: Axis + + /// Creates parameters for spacing styling + /// + /// - Parameters: + /// - length: The spacing value + /// - axis: The axis to apply the spacing to + public init( + length: Int? = 4, + axis: Axis = .both + ) { + self.length = length + self.axis = axis + } + + /// Creates parameters from a StyleParameters container + /// + /// - Parameter params: The style parameters container + /// - Returns: SpacingStyleOperation.Parameters + public static func from(_ params: StyleParameters) -> Parameters { + Parameters( + length: params.get("length"), + axis: params.get("axis", default: .both) + ) + } + } + + /// Applies the spacing style and returns the appropriate CSS classes + /// + /// - Parameter params: The parameters for spacing styling + /// - Returns: An array of CSS class names to be applied + public func applyClasses(params: Parameters) -> [String] { + guard let length = params.length else { + return [] + } + + switch params.axis { + case .horizontal: + return ["space-x-\(length)"] + case .vertical: + return ["space-y-\(length)"] + case .both: + return ["space-x-\(length)", "space-y-\(length)"] + } + } + + /// Shared instance for use across the framework + public static let shared = SpacingStyleOperation() + + /// Private initializer to enforce singleton usage + private init() {} +} + +// Extension for Element to provide spacing styling +extension Element { + /// Applies spacing between child elements horizontally and/or vertically. + /// + /// - Parameters: + /// - length: The spacing value in `0.25rem` increments. + /// - direction: The direction(s) to apply spacing (`horizontal`, `vertical`, or both). + /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. + /// - Returns: A new element with updated spacing classes. + public func spacing( + of length: Int? = 4, + along direction: Axis = .both, + on modifiers: Modifier... + ) -> Element { + let params = SpacingStyleOperation.Parameters( + length: length, + axis: direction + ) + + return SpacingStyleOperation.shared.applyToElement( + self, + params: params, + modifiers: modifiers + ) + } +} + +// Extension for ResponsiveBuilder to provide spacing styling +extension ResponsiveBuilder { + /// Applies spacing between child elements in a responsive context. + /// + /// - Parameters: + /// - length: The spacing value in `0.25rem` increments. + /// - direction: The direction(s) to apply spacing (`horizontal`, `vertical`, or both). + /// - Returns: The builder for method chaining. + @discardableResult + public func spacing( + of length: Int? = 4, + along direction: Axis = .both + ) -> ResponsiveBuilder { + let params = SpacingStyleOperation.Parameters( + length: length, + axis: direction + ) + + return SpacingStyleOperation.shared.applyToBuilder(self, params: params) + } +} + +// Global function for Declarative DSL +/// Applies spacing styling in the responsive context with Declarative syntax. +/// +/// - Parameters: +/// - length: The spacing value. +/// - direction: The axis to apply spacing to. +/// - Returns: A responsive modification for spacing. +public func spacing( + of length: Int? = 4, + along direction: Axis = .both +) -> ResponsiveModification { + let params = SpacingStyleOperation.Parameters( + length: length, + axis: direction + ) + + return SpacingStyleOperation.shared.asModification(params: params) +} diff --git a/Sources/WebUI/Styles/Base/Utilities.swift b/Sources/WebUI/Styles/Base/Utilities.swift index 773d02e8..24f6d953 100644 --- a/Sources/WebUI/Styles/Base/Utilities.swift +++ b/Sources/WebUI/Styles/Base/Utilities.swift @@ -73,12 +73,97 @@ public enum Modifier: String { /// Use to create dark theme variants of your UI elements. case dark + /// Applies the style to the first child element. + /// + /// Use to style the first item in a list or container. + case first + + /// Applies the style to the last child element. + /// + /// Use to style the last item in a list or container. + case last + + /// Applies the style when the element is disabled. + /// + /// Use to provide visual feedback for disabled form elements and controls. + case disabled + + /// Applies the style when the user prefers reduced motion. + /// + /// Use to create alternative animations or transitions for users who prefer reduced motion. + case motionReduce = "motion-reduce" + + /// Applies the style when the element has aria-busy="true". + /// + /// Use to style elements that are in a busy or loading state. + case ariaBusy = "aria-busy" + + /// Applies the style when the element has aria-checked="true". + /// + /// Use to style elements that represent a checked state, like checkboxes. + case ariaChecked = "aria-checked" + + /// Applies the style when the element has aria-disabled="true". + /// + /// Use to style elements that are disabled via ARIA attributes. + case ariaDisabled = "aria-disabled" + + /// Applies the style when the element has aria-expanded="true". + /// + /// Use to style elements that can be expanded, like accordions or dropdowns. + case ariaExpanded = "aria-expanded" + + /// Applies the style when the element has aria-hidden="true". + /// + /// Use to style elements that are hidden from screen readers. + case ariaHidden = "aria-hidden" + + /// Applies the style when the element has aria-pressed="true". + /// + /// Use to style elements that represent a pressed state, like toggle buttons. + case ariaPressed = "aria-pressed" + + /// Applies the style when the element has aria-readonly="true". + /// + /// Use to style elements that are in a read-only state. + case ariaReadonly = "aria-readonly" + + /// Applies the style when the element has aria-required="true". + /// + /// Use to style elements that are required, like form inputs. + case ariaRequired = "aria-required" + + /// Applies the style when the element has aria-selected="true". + /// + /// Use to style elements that are in a selected state, like tabs or menu items. + case ariaSelected = "aria-selected" + public var rawValue: String { switch self { - case .xs, .sm, .md, .lg, .xl, .hover, .focus, .active, .placeholder, .dark: + case .xs, .sm, .md, .lg, .xl, .hover, .focus, .active, .placeholder, .dark, .first, .last, .disabled: return "\(self):" case .xl2: return "2xl:" + case .motionReduce: + return "motion-reduce:" + case .ariaBusy: + return "aria-busy:" + case .ariaChecked: + return "aria-checked:" + case .ariaDisabled: + return "aria-disabled:" + case .ariaExpanded: + return "aria-expanded:" + case .ariaHidden: + return "aria-hidden:" + case .ariaPressed: + return "aria-pressed:" + case .ariaReadonly: + return "aria-readonly:" + case .ariaRequired: + return "aria-required:" + case .ariaSelected: + return "aria-selected:" } } } @@ -140,19 +225,19 @@ public enum Edge: String { /// ## Example /// ```swift /// Stack() -/// .overflow(.hidden, axis: .x) // Hide horizontal overflow only +/// .overflow(.hidden, axis: .horizontal) // Hide horizontal overflow only /// .spacing(.y, length: 4) // Add vertical spacing between children /// ``` public enum Axis: String { /// Applies to the horizontal (x) axis only. /// /// Use when you need to control behavior along the left-right direction. - case x + case horizontal = "x" /// Applies to the vertical (y) axis only. /// /// Use when you need to control behavior along the top-bottom direction. - case y + case vertical = "y" /// Applies to both horizontal and vertical axes simultaneously. /// @@ -193,3 +278,284 @@ public func combineClasses(_ baseClasses: [String], withModifiers modifiers: [Mo let modifierPrefix = modifiers.map { $0.rawValue }.joined() return baseClasses.map { "\(modifierPrefix)\($0)" } } + +/// Represents a color value for styling HTML elements. +/// +/// Defines a comprehensive palette of colors with standardized shades, optional opacity levels, +/// and a custom option for arbitrary values. These colors map directly to Tailwind CSS +/// color classes when applied to elements. +/// +/// ## Example +/// ```swift +/// Button(type: .submit) { "Save" } +/// .background(color: .blue(.500)) +/// .font(color: .white) +/// ``` +public enum Color { + /// A slate gray color with varying intensity shades and optional opacity. + /// + /// A cool gray with subtle blue undertones. + case slate(Shade, opacity: Double? = nil) + + /// A neutral gray color with varying intensity shades and optional opacity. + /// + /// A pure, balanced gray without strong undertones. + case gray(Shade, opacity: Double? = nil) + + /// A cool-toned gray color with varying intensity shades and optional opacity. + /// + /// A bluish-gray resembling zinc metal. + case zinc(Shade, opacity: Double? = nil) + + /// A balanced neutral color with varying intensity shades and optional opacity. + /// + /// A truly neutral gray without warm or cool undertones. + case neutral(Shade, opacity: Double? = nil) + + /// A warm-toned stone color with varying intensity shades and optional opacity. + /// + /// A brownish-gray resembling natural stone. + case stone(Shade, opacity: Double? = nil) + + /// A vibrant red color with varying intensity shades and optional opacity. + /// + /// A pure red that stands out for attention, warnings, and errors. + case red(Shade, opacity: Double? = nil) + + /// A bright orange color with varying intensity shades and optional opacity. + /// + /// A warm, energetic color between red and yellow. + case orange(Shade, opacity: Double? = nil) + + /// A rich amber color with varying intensity shades and optional opacity. + /// + /// A golden yellow-orange resembling amber gemstones. + case amber(Shade, opacity: Double? = nil) + + /// A sunny yellow color with varying intensity shades and optional opacity. + /// + /// A bright, attention-grabbing primary color. + case yellow(Shade, opacity: Double? = nil) + + /// A fresh lime color with varying intensity shades and optional opacity. + /// + /// A vibrant yellowish-green color. + case lime(Shade, opacity: Double? = nil) + + /// A lush green color with varying intensity shades and optional opacity. + /// + /// A balanced green suitable for success states and environmental themes. + case green(Shade, opacity: Double? = nil) + + /// A deep emerald color with varying intensity shades and optional opacity. + /// + /// A rich green with blue undertones resembling emerald gemstones. + case emerald(Shade, opacity: Double? = nil) + + /// A teal blue-green color with varying intensity shades and optional opacity. + /// + /// A balanced blue-green color with elegant properties. + case teal(Shade, opacity: Double? = nil) + + /// A bright cyan color with varying intensity shades and optional opacity. + /// + /// A vivid blue-green color with high visibility. + case cyan(Shade, opacity: Double? = nil) + + /// A soft sky blue color with varying intensity shades and optional opacity. + /// + /// A light blue resembling a clear sky. + case sky(Shade, opacity: Double? = nil) + + /// A classic blue color with varying intensity shades and optional opacity. + /// + /// A primary blue suitable for interfaces, links, and buttons. + case blue(Shade, opacity: Double? = nil) + + /// A rich indigo color with varying intensity shades and optional opacity. + /// + /// A deep blue-purple resembling indigo dye. + case indigo(Shade, opacity: Double? = nil) + + /// A vibrant violet color with varying intensity shades and optional opacity. + /// + /// A bright purple with strong blue undertones. + case violet(Shade, opacity: Double? = nil) + + /// A deep purple color with varying intensity shades and optional opacity. + /// + /// A rich mixture of red and blue with royal connotations. + case purple(Shade, opacity: Double? = nil) + + /// A bold fuchsia color with varying intensity shades and optional opacity. + /// + /// A vivid purple-red color with high contrast. + case fuchsia(Shade, opacity: Double? = nil) + + /// A soft pink color with varying intensity shades and optional opacity. + /// + /// A light red with warm, gentle appearance. + case pink(Shade, opacity: Double? = nil) + + /// A warm rose color with varying intensity shades and optional opacity. + /// + /// A deep pink resembling rose flowers. + case rose(Shade, opacity: Double? = nil) + + /// A custom color defined by a raw CSS value with optional opacity. + /// + /// Allows using arbitrary color values not included in the standard palette. + /// + /// - Parameters: + /// - String: A valid CSS color value (hex, RGB, HSL, or named color). + /// - opacity: Optional opacity value between 0 and 1. + /// + /// + /// ## Example + /// ```swift + /// Color.custom("#00aabb") + /// Color.custom("rgb(100, 150, 200)", opacity: 0.5) + /// ``` + case custom(String, opacity: Double? = nil) + + /// Defines shade intensity for colors in a consistent scale. + /// + /// Specifies a standardized range of shades from lightest (50) to darkest (950), + /// providing fine-grained control over color intensity. The scale follows a + /// consistent progression where lower numbers are lighter and higher numbers are darker. + /// + /// + /// ## Example + /// ```swift + /// // Light blue background with dark blue text + /// Stack() + /// .background(color: .blue(._100)) + /// .font(color: .blue(._800)) + /// ``` + public enum Shade: Int { + /// The lightest shade (50), typically a very faint tint. + /// + /// Best used for subtle backgrounds, hover states on light themes, or decorative elements. + case _50 = 50 + + /// A light shade (100), slightly more pronounced than 50. + /// + /// Suitable for light backgrounds, hover states, or highlighting selected items. + case _100 = 100 + + /// A subtle shade (200), often used for backgrounds or hover states. + /// + /// Good for secondary backgrounds, alternating rows, or light borders. + case _200 = 200 + + /// A moderate shade (300), suitable for borders or accents. + /// + /// Effective for dividers, borders, or subtle accent elements. + case _300 = 300 + + /// A balanced shade (400), often used for text or UI elements. + /// + /// Works well for secondary text, icons, or medium-emphasis UI elements. + case _400 = 400 + + /// A medium shade (500), commonly the default for a color family. + /// + /// The standard intensity, ideal for primary UI elements like buttons or indicators. + case _500 = 500 + + /// A slightly darker shade (600), good for emphasis. + /// + /// Useful for hover states on colored elements or medium-emphasis text. + case _600 = 600 + + /// A dark shade (700), often used for active states. + /// + /// Effective for active states, pressed buttons, or high-emphasis UI elements. + case _700 = 700 + + /// A very dark shade (800), suitable for strong contrast. + /// + /// Good for high-contrast text on light backgrounds or dark UI elements. + case _800 = 800 + + /// An almost black shade (900), often for deep backgrounds. + /// + /// Excellent for very dark backgrounds or high-contrast text elements. + case _900 = 900 + + /// The darkest shade (950), nearly black with a hint of color. + /// + /// The darkest variant, useful for the most intense applications of a color. + case _950 = 950 + } + + /// Provides the raw CSS class value for the color and opacity. + /// + /// Generates the appropriate string value for use in CSS class names, + /// including opacity formatting when specified. + /// + /// - Returns: A string representing the color in CSS class format. + /// + /// + /// ## Example + /// ```swift + /// let color = Color.blue(._500, opacity: 0.75) + /// let value = color.rawValue // Returns "blue-500/75" + /// ``` + public var rawValue: String { + func formatOpacity(_ opacity: Double?) -> String { + guard let opacity = opacity, (0...1).contains(opacity) else { return "" } + return "/\(Int(opacity * 100))" + } + + switch self { + case .slate(let shade, let opacity): + return "slate-\(shade.rawValue)\(formatOpacity(opacity))" + case .gray(let shade, let opacity): + return "gray-\(shade.rawValue)\(formatOpacity(opacity))" + case .zinc(let shade, let opacity): + return "zinc-\(shade.rawValue)\(formatOpacity(opacity))" + case .neutral(let shade, let opacity): + return "neutral-\(shade.rawValue)\(formatOpacity(opacity))" + case .stone(let shade, let opacity): + return "stone-\(shade.rawValue)\(formatOpacity(opacity))" + case .red(let shade, let opacity): + return "red-\(shade.rawValue)\(formatOpacity(opacity))" + case .orange(let shade, let opacity): + return "orange-\(shade.rawValue)\(formatOpacity(opacity))" + case .amber(let shade, let opacity): + return "amber-\(shade.rawValue)\(formatOpacity(opacity))" + case .yellow(let shade, let opacity): + return "yellow-\(shade.rawValue)\(formatOpacity(opacity))" + case .lime(let shade, let opacity): + return "lime-\(shade.rawValue)\(formatOpacity(opacity))" + case .green(let shade, let opacity): + return "green-\(shade.rawValue)\(formatOpacity(opacity))" + case .emerald(let shade, let opacity): + return "emerald-\(shade.rawValue)\(formatOpacity(opacity))" + case .teal(let shade, let opacity): + return "teal-\(shade.rawValue)\(formatOpacity(opacity))" + case .cyan(let shade, let opacity): + return "cyan-\(shade.rawValue)\(formatOpacity(opacity))" + case .sky(let shade, let opacity): + return "sky-\(shade.rawValue)\(formatOpacity(opacity))" + case .blue(let shade, let opacity): + return "blue-\(shade.rawValue)\(formatOpacity(opacity))" + case .indigo(let shade, let opacity): + return "indigo-\(shade.rawValue)\(formatOpacity(opacity))" + case .violet(let shade, let opacity): + return "violet-\(shade.rawValue)\(formatOpacity(opacity))" + case .purple(let shade, let opacity): + return "purple-\(shade.rawValue)\(formatOpacity(opacity))" + case .fuchsia(let shade, let opacity): + return "fuchsia-\(shade.rawValue)\(formatOpacity(opacity))" + case .pink(let shade, let opacity): + return "pink-\(shade.rawValue)\(formatOpacity(opacity))" + case .rose(let shade, let opacity): + return "rose-\(shade.rawValue)\(formatOpacity(opacity))" + case .custom(let value, let opacity): + let opacityStr = formatOpacity(opacity) + return "[\(value)]\(opacityStr)" + } + } +} diff --git a/Sources/WebUI/Styles/Core/InteractionModifiers.swift b/Sources/WebUI/Styles/Core/InteractionModifiers.swift new file mode 100644 index 00000000..ed1f2f1e --- /dev/null +++ b/Sources/WebUI/Styles/Core/InteractionModifiers.swift @@ -0,0 +1,370 @@ +import Foundation + +/// Provides support for interactive states and states modifiers in the WebUI framework. +/// +/// This extension adds support for additional modifiers like hover, focus, and other +/// interactive states to the ResponsiveBuilder to allow styling based on element state. + +extension ResponsiveBuilder { + /// Applies styles when the element is hovered. + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func hover(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .hover + modifications(self) + applyBreakpoint() + return self + } + + /// Applies styles when the element has keyboard focus. + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func focus(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .focus + modifications(self) + applyBreakpoint() + return self + } + + /// Applies styles when the element is being actively pressed or clicked. + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func active(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .active + modifications(self) + applyBreakpoint() + return self + } + + /// Applies styles to input placeholders within the element. + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func placeholder(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .placeholder + modifications(self) + applyBreakpoint() + return self + } + + /// Applies styles when dark mode is active. + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func dark(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .dark + modifications(self) + applyBreakpoint() + return self + } + + /// Applies styles to the first child element. + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func first(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .first + modifications(self) + applyBreakpoint() + return self + } + + /// Applies styles to the last child element. + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func last(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .last + modifications(self) + applyBreakpoint() + return self + } + + /// Applies styles when the element is disabled. + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func disabled(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .disabled + modifications(self) + applyBreakpoint() + return self + } + + /// Applies styles when the user prefers reduced motion. + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func motionReduce(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .motionReduce + modifications(self) + applyBreakpoint() + return self + } + + /// Applies styles when the element has aria-busy="true". + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func ariaBusy(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .ariaBusy + modifications(self) + applyBreakpoint() + return self + } + + /// Applies styles when the element has aria-checked="true". + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func ariaChecked(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .ariaChecked + modifications(self) + applyBreakpoint() + return self + } + + /// Applies styles when the element has aria-disabled="true". + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func ariaDisabled(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .ariaDisabled + modifications(self) + applyBreakpoint() + return self + } + + /// Applies styles when the element has aria-expanded="true". + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func ariaExpanded(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .ariaExpanded + modifications(self) + applyBreakpoint() + return self + } + + /// Applies styles when the element has aria-hidden="true". + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func ariaHidden(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .ariaHidden + modifications(self) + applyBreakpoint() + return self + } + + /// Applies styles when the element has aria-pressed="true". + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func ariaPressed(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .ariaPressed + modifications(self) + applyBreakpoint() + return self + } + + /// Applies styles when the element has aria-readonly="true". + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func ariaReadonly(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .ariaReadonly + modifications(self) + applyBreakpoint() + return self + } + + /// Applies styles when the element has aria-required="true". + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func ariaRequired(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .ariaRequired + modifications(self) + applyBreakpoint() + return self + } + + /// Applies styles when the element has aria-selected="true". + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func ariaSelected(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .ariaSelected + modifications(self) + applyBreakpoint() + return self + } +} + +// MARK: - Responsive DSL Functions + +/// Creates a hover state responsive modification. +/// +/// - Parameter content: A closure containing style modifications for hover state. +/// - Returns: A responsive modification for the hover state. +public func hover(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .hover, styleModification: content()) +} + +/// Creates a focus state responsive modification. +/// +/// - Parameter content: A closure containing style modifications for focus state. +/// - Returns: A responsive modification for the focus state. +public func focus(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .focus, styleModification: content()) +} + +/// Creates an active state responsive modification. +/// +/// - Parameter content: A closure containing style modifications for active state. +/// - Returns: A responsive modification for the active state. +public func active(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .active, styleModification: content()) +} + +/// Creates a placeholder responsive modification. +/// +/// - Parameter content: A closure containing style modifications for placeholder text. +/// - Returns: A responsive modification for placeholder text. +public func placeholder(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .placeholder, styleModification: content()) +} + +/// Creates a dark mode responsive modification. +/// +/// - Parameter content: A closure containing style modifications for dark mode. +/// - Returns: A responsive modification for dark mode. +public func dark(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .dark, styleModification: content()) +} + +/// Creates a first-child responsive modification. +/// +/// - Parameter content: A closure containing style modifications for first child elements. +/// - Returns: A responsive modification for first child elements. +public func first(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .first, styleModification: content()) +} + +/// Creates a last-child responsive modification. +/// +/// - Parameter content: A closure containing style modifications for last child elements. +/// - Returns: A responsive modification for last child elements. +public func last(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .last, styleModification: content()) +} + +/// Creates a disabled state responsive modification. +/// +/// - Parameter content: A closure containing style modifications for disabled state. +/// - Returns: A responsive modification for the disabled state. +public func disabled(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .disabled, styleModification: content()) +} + +/// Creates a motion-reduce responsive modification. +/// +/// - Parameter content: A closure containing style modifications for when users prefer reduced motion. +/// - Returns: A responsive modification for reduced motion preferences. +public func motionReduce(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .motionReduce, styleModification: content()) +} + +/// Creates an aria-busy responsive modification. +/// +/// - Parameter content: A closure containing style modifications for aria-busy="true". +/// - Returns: A responsive modification for the aria-busy state. +public func ariaBusy(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .ariaBusy, styleModification: content()) +} + +/// Creates an aria-checked responsive modification. +/// +/// - Parameter content: A closure containing style modifications for aria-checked="true". +/// - Returns: A responsive modification for the aria-checked state. +public func ariaChecked(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .ariaChecked, styleModification: content()) +} + +/// Creates an aria-disabled responsive modification. +/// +/// - Parameter content: A closure containing style modifications for aria-disabled="true". +/// - Returns: A responsive modification for the aria-disabled state. +public func ariaDisabled(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .ariaDisabled, styleModification: content()) +} + +/// Creates an aria-expanded responsive modification. +/// +/// - Parameter content: A closure containing style modifications for aria-expanded="true". +/// - Returns: A responsive modification for the aria-expanded state. +public func ariaExpanded(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .ariaExpanded, styleModification: content()) +} + +/// Creates an aria-hidden responsive modification. +/// +/// - Parameter content: A closure containing style modifications for aria-hidden="true". +/// - Returns: A responsive modification for the aria-hidden state. +public func ariaHidden(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .ariaHidden, styleModification: content()) +} + +/// Creates an aria-pressed responsive modification. +/// +/// - Parameter content: A closure containing style modifications for aria-pressed="true". +/// - Returns: A responsive modification for the aria-pressed state. +public func ariaPressed(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .ariaPressed, styleModification: content()) +} + +/// Creates an aria-readonly responsive modification. +/// +/// - Parameter content: A closure containing style modifications for aria-readonly="true". +/// - Returns: A responsive modification for the aria-readonly state. +public func ariaReadonly(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .ariaReadonly, styleModification: content()) +} + +/// Creates an aria-required responsive modification. +/// +/// - Parameter content: A closure containing style modifications for aria-required="true". +/// - Returns: A responsive modification for the aria-required state. +public func ariaRequired(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .ariaRequired, styleModification: content()) +} + +/// Creates an aria-selected responsive modification. +/// +/// - Parameter content: A closure containing style modifications for aria-selected="true". +/// - Returns: A responsive modification for the aria-selected state. +public func ariaSelected(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .ariaSelected, styleModification: content()) +} \ No newline at end of file diff --git a/Sources/WebUI/Styles/Core/ResponsiveAlias.swift b/Sources/WebUI/Styles/Core/ResponsiveAlias.swift new file mode 100644 index 00000000..2c0972b1 --- /dev/null +++ b/Sources/WebUI/Styles/Core/ResponsiveAlias.swift @@ -0,0 +1,15 @@ +import Foundation + +/// Provides a backward compatibility alias for the new responsive styling API +extension Element { + /// Alias for the `.on` method to maintain backward compatibility with existing code + /// + /// This method provides a backward-compatible way to use the new `.on` method + /// with code that was written to use `.responsive`. + /// + /// - Parameter content: A closure defining responsive style configurations using the result builder. + /// - Returns: An element with responsive styles applied. + public func responsive(@ResponsiveStyleBuilder _ content: () -> ResponsiveModification) -> Element { + return on(content) + } +} \ No newline at end of file diff --git a/Sources/WebUI/Styles/Core/ResponsiveModifier.swift b/Sources/WebUI/Styles/Core/ResponsiveModifier.swift new file mode 100644 index 00000000..014bc1a5 --- /dev/null +++ b/Sources/WebUI/Styles/Core/ResponsiveModifier.swift @@ -0,0 +1,225 @@ +import Foundation + +/// Provides a block-based responsive design API for WebUI elements. +/// +/// The on modifier allows developers to define responsive styles +/// across different breakpoints in a single, concise block, creating cleaner +/// and more maintainable code for responsive designs. +/// +/// ## Example +/// ```swift +/// Text { "Responsive Content" } +/// .font(size: .sm) +/// .background(color: .neutral(._500)) +/// .on { +/// md { +/// font(size: .lg) +/// background(color: .neutral(._700)) +/// padding(of: 4) +/// } +/// lg { +/// font(size: .xl) +/// background(color: .neutral(._900)) +/// font(alignment: .center) +/// } +/// } +/// ``` +extension Element { + /// Applies responsive styling across different breakpoints with a declarative syntax. + /// + /// This method provides a clean, declarative way to define styles for multiple + /// breakpoints in a single block, improving code readability and maintainability. + /// + /// - Parameter content: A closure defining responsive style configurations using the result builder. + /// - Returns: An element with responsive styles applied. + /// + /// ## Example + /// ```swift + /// Button { "Submit" } + /// .background(color: .blue(._500)) + /// .on { + /// sm { + /// padding(of: 2) + /// font(size: .sm) + /// } + /// md { + /// padding(of: 4) + /// font(size: .base) + /// } + /// lg { + /// padding(of: 6) + /// font(size: .lg) + /// } + /// } + /// ``` + public func on(@ResponsiveStyleBuilder _ content: () -> ResponsiveModification) -> Element { + let builder = ResponsiveBuilder(element: self) + let modification = content() + modification.apply(to: builder) + return builder.element + } +} + +/// Builds responsive style configurations for elements across different breakpoints. +/// +/// `ResponsiveBuilder` provides a fluent, method-chaining API for applying style +/// modifications at specific screen sizes. Each method represents a breakpoint +/// and accepts a closure where style modifications can be defined. +/// +/// This class is not typically created directly, but instead used through the +/// `Element.on(_:)` method. +public class ResponsiveBuilder { + /// The current element being modified + var element: Element + /// Keep track of responsive styles for each breakpoint + internal var pendingClasses: [String] = [] + /// The current breakpoint being modified + internal var currentBreakpoint: Modifier? + + /// Creates a new responsive builder for the given element. + /// + /// - Parameter element: The element to apply responsive styles to. + init(element: Element) { + self.element = element + } + + /// Applies styles at the extra-small breakpoint (480px+). + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func xs(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .xs + modifications(self) + applyBreakpoint() + return self + } + + /// Applies styles at the small breakpoint (640px+). + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func sm(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .sm + modifications(self) + applyBreakpoint() + return self + } + + /// Applies styles at the medium breakpoint (768px+). + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func md(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .md + modifications(self) + applyBreakpoint() + return self + } + + /// Applies styles at the large breakpoint (1024px+). + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func lg(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .lg + modifications(self) + applyBreakpoint() + return self + } + + /// Applies styles at the extra-large breakpoint (1280px+). + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func xl(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .xl + modifications(self) + applyBreakpoint() + return self + } + + /// Applies styles at the 2x-extra-large breakpoint (1536px+). + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func xl2(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .xl2 + modifications(self) + applyBreakpoint() + return self + } + + /// Applies the breakpoint prefix to all pending classes and add them to the element + internal func applyBreakpoint() { + guard let breakpoint = currentBreakpoint else { return } + + // Apply the breakpoint prefix to all pending classes + let responsiveClasses = pendingClasses.map { + // Handle duplication for flex-*, justify-*, items-* + if $0.starts(with: "flex-") || $0.starts(with: "justify-") || $0.starts(with: "items-") + || $0.starts(with: "grid-") + { + return "\(breakpoint.rawValue)\($0)" + } else if $0 == "flex" || $0 == "grid" { + return "\(breakpoint.rawValue)\($0)" + } else { + return "\(breakpoint.rawValue)\($0)" + } + } + + // Add the responsive classes to the element + self.element = Element( + tag: self.element.tag, + id: self.element.id, + classes: (self.element.classes ?? []) + responsiveClasses, + role: self.element.role, + label: self.element.label, + data: self.element.data, + isSelfClosing: self.element.isSelfClosing, + customAttributes: self.element.customAttributes, + content: self.element.contentBuilder + ) + + // Clear pending classes for the next breakpoint + pendingClasses = [] + currentBreakpoint = nil + } + + /// Add a class to the pending list of classes + public func addClass(_ className: String) { + pendingClasses.append(className) + } +} + +// Font styling methods +extension ResponsiveBuilder { + @discardableResult + public func size(_ value: Int) -> ResponsiveBuilder { + addClass("size-\(value)") + return self + } + + @discardableResult + public func frame( + width: Int? = nil, + height: Int? = nil, + minWidth: Int? = nil, + maxWidth: Int? = nil, + minHeight: Int? = nil, + maxHeight: Int? = nil + ) -> ResponsiveBuilder { + if let width = width { addClass("w-\(width)") } + if let height = height { addClass("h-\(height)") } + if let minWidth = minWidth { addClass("min-w-\(minWidth)") } + if let maxWidth = maxWidth { addClass("max-w-\(maxWidth)") } + if let minHeight = minHeight { addClass("min-h-\(minHeight)") } + if let maxHeight = maxHeight { addClass("max-h-\(maxHeight)") } + return self + } +} diff --git a/Sources/WebUI/Styles/Core/ResponsiveStyleBuilder.swift b/Sources/WebUI/Styles/Core/ResponsiveStyleBuilder.swift new file mode 100644 index 00000000..9ca7e177 --- /dev/null +++ b/Sources/WebUI/Styles/Core/ResponsiveStyleBuilder.swift @@ -0,0 +1,75 @@ +import Foundation + +/// A result builder for creating responsive styles with a clean, SwiftUI-like syntax. +/// +/// This builder enables a more natural way to define responsive styles without using `$0` references. +/// +/// ## Example +/// ```swift +/// Element(tag: "div") +/// .responsive { +/// sm { +/// font(size: .base) +/// } +/// md { +/// font(size: .lg) +/// background(color: .blue(._500)) +/// } +/// } +/// ``` +@resultBuilder +public struct ResponsiveStyleBuilder { + /// Builds an empty responsive style. + public static func buildBlock() -> ResponsiveModification { + EmptyResponsiveModification() + } + + /// Builds a responsive style from multiple modifications. + public static func buildBlock(_ components: ResponsiveModification...) -> ResponsiveModification { + CompositeResponsiveModification(modifications: components) + } + + /// Transforms an optional into a responsive modification. + public static func buildOptional(_ component: ResponsiveModification?) -> ResponsiveModification { + component ?? EmptyResponsiveModification() + } + + /// Transforms an either-or condition into a responsive modification. + public static func buildEither(first component: ResponsiveModification) -> ResponsiveModification { + component + } + + /// Transforms an either-or condition into a responsive modification. + public static func buildEither(second component: ResponsiveModification) -> ResponsiveModification { + component + } + + /// Transforms an array of responsive modifications into a single modification. + public static func buildArray(_ components: [ResponsiveModification]) -> ResponsiveModification { + CompositeResponsiveModification(modifications: components) + } +} + +/// Protocol defining the interface for responsive style modifications. +public protocol ResponsiveModification { + /// Applies the modification to the given responsive builder. + func apply(to builder: ResponsiveBuilder) +} + +/// Represents an empty responsive modification. +struct EmptyResponsiveModification: ResponsiveModification { + func apply(to builder: ResponsiveBuilder) { + // Do nothing for empty modifications + } +} + +/// Represents a composite of multiple responsive modifications. +struct CompositeResponsiveModification: ResponsiveModification { + let modifications: [ResponsiveModification] + + func apply(to builder: ResponsiveBuilder) { + for modification in modifications { + modification.apply(to: builder) + } + } +} diff --git a/Sources/WebUI/Styles/Core/ResponsiveStyleModifiers.swift b/Sources/WebUI/Styles/Core/ResponsiveStyleModifiers.swift new file mode 100644 index 00000000..d0d9b45d --- /dev/null +++ b/Sources/WebUI/Styles/Core/ResponsiveStyleModifiers.swift @@ -0,0 +1,214 @@ +import Foundation + +/// Provides the implementation for breakpoint and interactive state modifiers in the responsive DSL. +/// +/// These functions are available in the context of a responsive closure, allowing +/// for a more natural, SwiftUI-like syntax without requiring `$0` references. +public struct BreakpointModification: ResponsiveModification { + private let breakpoint: Modifier + private let styleModification: ResponsiveModification + + init(breakpoint: Modifier, styleModification: ResponsiveModification) { + self.breakpoint = breakpoint + self.styleModification = styleModification + } + + public func apply(to builder: ResponsiveBuilder) { + switch breakpoint { + case .xs: + builder.xs { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .sm: + builder.sm { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .md: + builder.md { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .lg: + builder.lg { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .xl: + builder.xl { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .xl2: + builder.xl2 { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .hover: + builder.hover { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .focus: + builder.focus { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .active: + builder.active { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .placeholder: + builder.placeholder { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .dark: + builder.dark { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .first: + builder.first { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .last: + builder.last { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .disabled: + builder.disabled { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .motionReduce: + builder.motionReduce { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .ariaBusy: + builder.ariaBusy { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .ariaChecked: + builder.ariaChecked { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .ariaDisabled: + builder.ariaDisabled { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .ariaExpanded: + builder.ariaExpanded { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .ariaHidden: + builder.ariaHidden { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .ariaPressed: + builder.ariaPressed { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .ariaReadonly: + builder.ariaReadonly { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .ariaRequired: + builder.ariaRequired { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .ariaSelected: + builder.ariaSelected { innerBuilder in + styleModification.apply(to: innerBuilder) + } + } + } +} + +/// Represents a style modification in the responsive DSL. +public struct StyleModification: ResponsiveModification { + private let modification: (ResponsiveBuilder) -> Void + + init(_ modification: @escaping (ResponsiveBuilder) -> Void) { + self.modification = modification + } + + public func apply(to builder: ResponsiveBuilder) { + modification(builder) + } +} + +// MARK: - Breakpoint Functions +// Note: Interactive state functions like hover, focus, etc. are defined in InteractionModifiers.swift + +/// Creates an extra-small breakpoint (480px+) responsive modification. +/// +/// - Parameter content: A closure containing style modifications for this breakpoint. +/// - Returns: A responsive modification for the extra-small breakpoint. +public func xs(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .xs, styleModification: content()) +} + +/// Creates a small breakpoint (640px+) responsive modification. +/// +/// - Parameter content: A closure containing style modifications for this breakpoint. +/// - Returns: A responsive modification for the small breakpoint. +public func sm(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .sm, styleModification: content()) +} + +/// Creates a medium breakpoint (768px+) responsive modification. +/// +/// - Parameter content: A closure containing style modifications for this breakpoint. +/// - Returns: A responsive modification for the medium breakpoint. +public func md(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .md, styleModification: content()) +} + +/// Creates a large breakpoint (1024px+) responsive modification. +/// +/// - Parameter content: A closure containing style modifications for this breakpoint. +/// - Returns: A responsive modification for the large breakpoint. +public func lg(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .lg, styleModification: content()) +} + +/// Creates an extra-large breakpoint (1280px+) responsive modification. +/// +/// - Parameter content: A closure containing style modifications for this breakpoint. +/// - Returns: A responsive modification for the extra-large breakpoint. +public func xl(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .xl, styleModification: content()) +} + +/// Creates a 2x extra-large breakpoint (1536px+) responsive modification. +/// +/// - Parameter content: A closure containing style modifications for this breakpoint. +/// - Returns: A responsive modification for the 2x extra-large breakpoint. +public func xl2(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .xl2, styleModification: content()) +} + +// MARK: - Style Modification Functions + +// Font styling is now implemented in FontStyleOperation.swift + +// Background styling is now implemented in BackgroundStyleOperation.swift + +// Padding styling is now implemented in PaddingStyleOperation.swift + +// Margins styling is now implemented in MarginsStyleOperation.swift + +// Border styling is now implemented in BorderStyleOperation.swift + +// Opacity styling is now implemented in OpacityStyleOperation.swift + +// Size styling is now implemented in SizingStyleOperation.swift + +// Frame styling is now implemented in SizingStyleOperation.swift + +// Flex styling is implemented in ResponsiveBuilder.swift + +// Grid styling is implemented in ResponsiveBuilder.swift + +// Position styling is now implemented in PositionStyleOperation.swift + +// Overflow styling is now implemented in OverflowStyleOperation.swift + +// Hidden styling is implemented in ResponsiveBuilder.swift + +// Border radius styling is now implemented in BorderRadiusStyleOperation.swift + +// Interactive state modifiers like hover, focus, etc. are implemented in InteractionModifiers.swift + +// ARIA state modifiers are implemented in InteractionModifiers.swift diff --git a/Sources/WebUI/Styles/Core/StyleOperation.swift b/Sources/WebUI/Styles/Core/StyleOperation.swift new file mode 100644 index 00000000..5757862f --- /dev/null +++ b/Sources/WebUI/Styles/Core/StyleOperation.swift @@ -0,0 +1,105 @@ +import Foundation + +/// Protocol defining a unified style operation that can be used across different styling contexts +/// +/// This protocol enables defining a style operation once and reusing it across Element methods +/// and the Declaritive DSL functions, ensuring consistency and maintainability. +public protocol StyleOperation { + /// The parameters type used by this style operation + associatedtype Parameters + + /// Applies the style operation and returns the appropriate CSS classes + /// + /// - Parameter params: The parameters for this style operation + /// - Returns: An array of CSS class names to be applied + func applyClasses(params: Parameters) -> [String] +} + +/// A container for style parameters to simplify parameter passing +public struct StyleParameters { + /// The underlying storage for parameters + private var values: [String: Any] = [:] + + /// Creates an empty parameters container + public init() {} + + /// Sets a parameter value + /// + /// - Parameters: + /// - key: The parameter name + /// - value: The parameter value + public mutating func set(_ key: String, value: T?) { + if let value = value { + values[key] = value + } + } + + /// Gets a parameter value + /// + /// - Parameter key: The parameter name + /// - Returns: The parameter value, or nil if not present + public func get(_ key: String) -> T? { + values[key] as? T + } + + /// Gets a parameter value with a default fallback + /// + /// - Parameters: + /// - key: The parameter name + /// - defaultValue: The default value to use if the parameter is not present + /// - Returns: The parameter value, or the default if not present + public func get(_ key: String, default defaultValue: T) -> T { + (values[key] as? T) ?? defaultValue + } +} + +/// A utility for adapting style operations to Element extensions +extension StyleOperation { + /// Adapts this style operation for use with Element extensions + /// + /// - Parameters: + /// - element: The element to apply styles to + /// - params: The parameters for this style operation + /// - modifiers: The modifiers to apply (e.g., .hover, .md) + /// - Returns: A new element with the styles applied + public func applyToElement(_ element: Element, params: Parameters, modifiers: [Modifier] = []) -> Element { + let classes = applyClasses(params: params) + let newClasses = combineClasses(classes, withModifiers: modifiers) + + return Element( + tag: element.tag, + id: element.id, + classes: (element.classes ?? []) + newClasses, + role: element.role, + label: element.label, + data: element.data, + isSelfClosing: element.isSelfClosing, + customAttributes: element.customAttributes, + content: element.contentBuilder + ) + } + + /// Internal adapter for use with the responsive builder + /// + /// - Parameters: + /// - builder: The responsive builder to apply styles to + /// - params: The parameters for this style operation + /// - Returns: The builder for method chaining + public func applyToBuilder(_ builder: ResponsiveBuilder, params: Parameters) -> ResponsiveBuilder { + let classes = applyClasses(params: params) + for className in classes { + builder.addClass(className) + } + return builder + } + + /// Adapts this style operation for use with the Declaritive result builder syntax + /// + /// - Parameter params: The parameters for this style operation + /// - Returns: A responsive modification that can be used in responsive blocks + public func asModification(params: Parameters) -> ResponsiveModification { + StyleModification { builder in + _ = applyToBuilder(builder, params: params) + } + } +} diff --git a/Sources/WebUI/Styles/Core/StyleRegistry.swift b/Sources/WebUI/Styles/Core/StyleRegistry.swift new file mode 100644 index 00000000..b6e1122d --- /dev/null +++ b/Sources/WebUI/Styles/Core/StyleRegistry.swift @@ -0,0 +1,101 @@ +import Foundation + +/// Registry for all style operations in the WebUI framework +/// +/// Provides a central point of access for all style operations, +/// ensuring that they are properly initialized and shared across the framework. +/// This enables a consistent styling system across both direct Element styling +/// and the Declaritive responsive syntax. +public enum StyleRegistry { + // Border styles + public static let border = BorderStyleOperation.shared + + // Border radius styles + public static let borderRadius = BorderRadiusStyleOperation.shared + + // Margin styles + public static let margins = MarginsStyleOperation.shared + + // Padding styles + public static let padding = PaddingStyleOperation.shared + + // Spacing styles + public static let spacing = SpacingStyleOperation.shared + + // Background styles + public static let background = BackgroundStyleOperation.shared + + // Opacity styles + public static let opacity = OpacityStyleOperation.shared + + // Font styles + public static let font = FontStyleOperation.shared + + // Display styles + public static let display = DisplayStyleOperation.shared + + // Position styles + public static let position = PositionStyleOperation.shared + + // ZIndex styles + public static let zIndex = ZIndexStyleOperation.shared + + // Overflow styles + public static let overflow = OverflowStyleOperation.shared + + // Cursor styles + public static let cursor = CursorStyleOperation.shared + + // Transform styles + public static let transform = TransformStyleOperation.shared + + // Transition styles + public static let transition = TransitionStyleOperation.shared + + // Scroll styles + public static let scroll = ScrollStyleOperation.shared + + // Sizing styles + public static let sizing = SizingStyleOperation.shared + + /// Initializes all style operations + /// + /// This method should be called during the framework initialization + /// to ensure that all style operations are properly initialized for + /// use with both Element methods and Declarative syntax. + public static func initialize() { + // The singleton pattern ensures that operations are only initialized once + // This method exists for explicit initialization if needed + _ = border + _ = borderRadius + _ = margins + _ = padding + _ = spacing + _ = background + _ = opacity + _ = font + _ = display + _ = position + _ = zIndex + _ = overflow + _ = cursor + _ = transform + _ = transition + _ = scroll + _ = sizing + } + + /// Registers a custom style operation + /// + /// This method allows for registering custom style operations + /// from outside the core framework, making them available for both + /// direct Element styling and declaritive responsive syntax. + /// + /// - Parameters: + /// - name: The name of the custom style operation + /// - operation: The style operation to register + public static func register(name: String, operation: T) { + // In a more complex implementation, this could store operations in a dictionary + // For now, this is a placeholder for future extensibility + } +} diff --git a/Sources/WebUI/Styles/Core/StyleUtilities.swift b/Sources/WebUI/Styles/Core/StyleUtilities.swift new file mode 100644 index 00000000..f6341d61 --- /dev/null +++ b/Sources/WebUI/Styles/Core/StyleUtilities.swift @@ -0,0 +1,55 @@ +import Foundation + +/// Utilities for working with style operations +public enum StyleUtilities { + /// Converts a varargs parameter to an array + /// + /// This is useful for converting the `edges: Edge...` parameter to an array + /// that can be used with style operations. + /// + /// - Parameter varargs: The varargs parameter + /// - Returns: An array containing the varargs elements, or [.all] if empty + public static func toArray(_ varargs: T...) -> [T] { + varargs.isEmpty ? [] : varargs + } + + /// Safely combines a class with modifiers + /// + /// This is a safer version of the combineClasses function that handles + /// nil or empty arrays gracefully. + /// + /// - Parameters: + /// - baseClass: The base class name + /// - modifiers: The modifiers to apply + /// - Returns: The combined class names + public static func combineClass(_ baseClass: String, withModifiers modifiers: [Modifier]) -> [String] { + if modifiers.isEmpty { + return [baseClass] + } + + let modifierPrefix = modifiers.map { modifier in modifier.rawValue }.joined() + return ["\(modifierPrefix)\(baseClass)"] + } + + /// Safely combines multiple classes with modifiers + /// + /// This is a safer version of the combineClasses function that handles + /// nil or empty arrays gracefully. + /// + /// - Parameters: + /// - baseClasses: The base class names + /// - modifiers: The modifiers to apply + /// - Returns: The combined class names + public static func combineClasses(_ baseClasses: [String], withModifiers modifiers: [Modifier]) -> [String] { + if baseClasses.isEmpty { + return [] + } + + if modifiers.isEmpty { + return baseClasses + } + + let modifierPrefix = modifiers.map { modifier in modifier.rawValue }.joined() + return baseClasses.map { baseClass in "\(modifierPrefix)\(baseClass)" } + } +} diff --git a/Sources/WebUI/Styles/Positioning/Overflow.swift b/Sources/WebUI/Styles/Positioning/Overflow.swift deleted file mode 100644 index 36e5cf1c..00000000 --- a/Sources/WebUI/Styles/Positioning/Overflow.swift +++ /dev/null @@ -1,65 +0,0 @@ -/// Defines overflow behavior options. -/// -/// Specifies how content exceeding an element's bounds is handled. -/// -/// ## Example -/// ```swift -/// Stack() { -/// // Long content that might overflow -/// Text { "This is a long text that might overflow its container..." } -/// } -/// .overflow(.hidden) -/// ``` -public enum OverflowType: String { - /// Automatically adds scrollbars when content overflows. - case auto - /// Clips overflowing content and hides it. - case hidden - /// Displays overflowing content without clipping. - case visible - /// Always adds scrollbars, even if content fits. - case scroll -} - -extension Element { - /// Applies overflow styling to the element. - /// - /// Sets how overflowing content is handled, optionally on a specific axis and with modifiers. - /// - /// - Parameters: - /// - type: Determines the overflow behavior (e.g., hidden, scroll). - /// - axis: Specifies the axis for overflow (defaults to both). - /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. - /// - Returns: A new element with updated overflow classes. - /// - /// ## Example - /// ```swift - /// Stack(classes: ["content-container"]) - /// .overflow(.scroll, axis: .y) - /// .frame(height: .spacing(300)) - /// - /// Stack(classes: ["image-container"]) - /// .overflow(.hidden) - /// .rounded(.lg) - /// ``` - public func overflow( - _ type: OverflowType, - axis: Axis = .both, - on modifiers: Modifier... - ) -> Element { - let axisString = axis.rawValue.isEmpty ? "" : "-\(axis.rawValue)" - let baseClass = "overflow\(axisString)-\(type.rawValue)" - let newClasses = combineClasses([baseClass], withModifiers: modifiers) - - return Element( - tag: self.tag, - id: self.id, - classes: (self.classes ?? []) + newClasses, - role: self.role, - label: self.label, - isSelfClosing: self.isSelfClosing, - customAttributes: self.customAttributes, - content: self.contentBuilder - ) - } -} diff --git a/Sources/WebUI/Styles/Positioning/Overflow/OverflowStyleOperation.swift b/Sources/WebUI/Styles/Positioning/Overflow/OverflowStyleOperation.swift new file mode 100644 index 00000000..8c23ff70 --- /dev/null +++ b/Sources/WebUI/Styles/Positioning/Overflow/OverflowStyleOperation.swift @@ -0,0 +1,136 @@ +import Foundation + +/// Style operation for overflow styling +/// +/// Provides a unified implementation for overflow styling that can be used across +/// Element methods and the Declarative DSL functions. +public struct OverflowStyleOperation: StyleOperation, @unchecked Sendable { + /// Parameters for overflow styling + public struct Parameters { + /// The overflow type + public let type: OverflowType + + /// The axis to apply overflow to + public let axis: Axis + + /// Creates parameters for overflow styling + /// + /// - Parameters: + /// - type: The overflow type + /// - axis: The axis to apply overflow to + public init( + type: OverflowType, + axis: Axis = .both + ) { + self.type = type + self.axis = axis + } + + /// Creates parameters from a StyleParameters container + /// + /// - Parameter params: The style parameters container + /// - Returns: OverflowStyleOperation.Parameters + public static func from(_ params: StyleParameters) -> Parameters { + Parameters( + type: params.get("type")!, + axis: params.get("axis", default: .both) + ) + } + } + + /// Applies the overflow style and returns the appropriate CSS classes + /// + /// - Parameter params: The parameters for overflow styling + /// - Returns: An array of CSS class names to be applied to elements + public func applyClasses(params: Parameters) -> [String] { + let axisString = params.axis.rawValue.isEmpty ? "" : "-\(params.axis.rawValue)" + return ["overflow\(axisString)-\(params.type.rawValue)"] + } + + /// Shared instance for use across the framework + public static let shared = OverflowStyleOperation() + + /// Private initializer to enforce singleton usage + private init() {} +} + +// Extension for Element to provide overflow styling +extension Element { + /// Applies overflow styling to the element. + /// + /// Sets how overflowing content is handled, optionally on a specific axis and with modifiers. + /// + /// - Parameters: + /// - type: Determines the overflow behavior (e.g., hidden, scroll). + /// - axis: Specifies the axis for overflow (defaults to both). + /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. + /// - Returns: A new element with updated overflow classes. + /// + /// ## Example + /// ```swift + /// Stack(classes: ["content-container"]) + /// .overflow(.scroll, axis: .horizontal) + /// .frame(height: .spacing(300)) + /// + /// Stack(classes: ["image-container"]) + /// .overflow(.hidden) + /// .rounded(.lg) + /// ``` + public func overflow( + _ type: OverflowType, + axis: Axis = .both, + on modifiers: Modifier... + ) -> Element { + let params = OverflowStyleOperation.Parameters( + type: type, + axis: axis + ) + + return OverflowStyleOperation.shared.applyToElement( + self, + params: params, + modifiers: modifiers + ) + } +} + +// Extension for ResponsiveBuilder to provide overflow styling +extension ResponsiveBuilder { + /// Applies overflow styling in a responsive context. + /// + /// - Parameters: + /// - type: The overflow type. + /// - axis: The axis to apply overflow to. + /// - Returns: The builder for method chaining. + @discardableResult + public func overflow( + _ type: OverflowType, + axis: Axis = .both + ) -> ResponsiveBuilder { + let params = OverflowStyleOperation.Parameters( + type: type, + axis: axis + ) + + return OverflowStyleOperation.shared.applyToBuilder(self, params: params) + } +} + +// Global function for Declarative DSL +/// Applies overflow styling in the responsive context. +/// +/// - Parameters: +/// - type: The overflow type. +/// - axis: The axis to apply overflow to. +/// - Returns: A responsive modification for overflow. +public func overflow( + _ type: OverflowType, + axis: Axis = .both +) -> ResponsiveModification { + let params = OverflowStyleOperation.Parameters( + type: type, + axis: axis + ) + + return OverflowStyleOperation.shared.asModification(params: params) +} diff --git a/Sources/WebUI/Styles/Positioning/Overflow/OverflowTypes.swift b/Sources/WebUI/Styles/Positioning/Overflow/OverflowTypes.swift new file mode 100644 index 00000000..f952d58d --- /dev/null +++ b/Sources/WebUI/Styles/Positioning/Overflow/OverflowTypes.swift @@ -0,0 +1,24 @@ +/// Defines overflow behavior options. +/// +/// Specifies how content exceeding an element's bounds is handled. +/// +/// ## Example +/// ```swift +/// Stack() { +/// // Long content that might overflow +/// Text { "This is a long text that might overflow its container..." } +/// } +/// .overflow(.hidden) +/// ``` +public enum OverflowType: String { + /// Automatically adds scrollbars when content overflows. + case auto + /// Clips overflowing content and hides it. + case hidden + /// Displays overflowing content without clipping. + case visible + /// Always adds scrollbars, even if content fits. + case scroll +} + +// Implementation has been moved to OverflowStyleOperation.swift diff --git a/Sources/WebUI/Styles/Positioning/Position.swift b/Sources/WebUI/Styles/Positioning/Position.swift deleted file mode 100644 index d345c061..00000000 --- a/Sources/WebUI/Styles/Positioning/Position.swift +++ /dev/null @@ -1,70 +0,0 @@ -/// Defines positioning types for elements. -/// -/// Specifies how an element is positioned within its parent or viewport. -public enum PositionType: String { - /// Positions the element using the normal document flow. - case `static` - /// Positions the element relative to its normal position. - case relative - /// Positions the element relative to its nearest positioned ancestor. - case absolute - /// Positions the element relative to the viewport and fixes it in place. - case fixed - /// Positions the element relative to its normal position and sticks on scroll. - case sticky -} - -extension Element { - /// Applies positioning styling to the element with one or more edges. - /// - /// Sets the position type and optional inset values for specified edges, scoped to modifiers if provided. - /// - /// - Parameters: - /// - type: Specifies the positioning method (e.g., absolute, fixed). - /// - edges: One or more edges to apply the inset to. Defaults to `.all`. - /// - length: The distance value for the specified edges (e.g., "0", "4", "auto"). - /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. - /// - Returns: A new element with updated position classes. - public func position( - _ type: PositionType? = nil, - at edges: Edge..., - offset length: Int? = nil, - on modifiers: Modifier... - ) -> Element { - let effectiveEdges = edges.isEmpty ? [Edge.all] : edges - var baseClasses: [String] = [type?.rawValue ?? ""] - - if let lengthValue = length { - for edge in effectiveEdges { - let edgePrefix: String - switch edge { - case .all: edgePrefix = "inset" - case .top: edgePrefix = "top" - case .leading: edgePrefix = "left" - case .trailing: edgePrefix = "right" - case .bottom: edgePrefix = "bottom" - case .horizontal: edgePrefix = "inset-x" - case .vertical: edgePrefix = "inset-y" - } - if lengthValue < 0 { - baseClasses.append("-\(edgePrefix)-\(abs(lengthValue))") - } else { - baseClasses.append("\(edgePrefix)-\(lengthValue)") - } - } - } - - let newClasses = combineClasses(baseClasses, withModifiers: modifiers) - - return Element( - tag: self.tag, - id: self.id, - classes: (self.classes ?? []) + newClasses, - role: self.role, - label: self.label, - isSelfClosing: self.isSelfClosing, - customAttributes: self.customAttributes, - content: self.contentBuilder - ) - } -} diff --git a/Sources/WebUI/Styles/Positioning/Position/PositionStyleOperation.swift b/Sources/WebUI/Styles/Positioning/Position/PositionStyleOperation.swift new file mode 100644 index 00000000..e244f8e0 --- /dev/null +++ b/Sources/WebUI/Styles/Positioning/Position/PositionStyleOperation.swift @@ -0,0 +1,167 @@ +import Foundation + +/// Style operation for positioning elements +/// +/// Provides a unified implementation for position styling that can be used across +/// Element methods and the Declarative DSL functions. +public struct PositionStyleOperation: StyleOperation, @unchecked Sendable { + /// Parameters for position styling + public struct Parameters { + /// The position type + public let type: PositionType? + + /// The edges to apply positioning to + public let edges: [Edge] + + /// The offset value for positioning + public let offset: Int? + + /// Creates parameters for position styling + /// + /// - Parameters: + /// - type: The position type + /// - edges: The edges to apply positioning to + /// - offset: The offset value for positioning + public init( + type: PositionType? = nil, + edges: [Edge] = [.all], + offset: Int? = nil + ) { + self.type = type + self.edges = edges.isEmpty ? [.all] : edges + self.offset = offset + } + + /// Creates parameters from a StyleParameters container + /// + /// - Parameter params: The style parameters container + /// - Returns: PositionStyleOperation.Parameters + public static func from(_ params: StyleParameters) -> Parameters { + Parameters( + type: params.get("type"), + edges: params.get("edges", default: [.all]), + offset: params.get("offset") + ) + } + } + + /// Applies the position style and returns the appropriate CSS classes + /// + /// - Parameter params: The parameters for position styling + /// - Returns: An array of CSS class names to be applied to elements + public func applyClasses(params: Parameters) -> [String] { + var classes: [String] = [] + + if let type = params.type { + classes.append(type.rawValue) + } + + if let offset = params.offset { + for edge in params.edges { + let edgePrefix: String + switch edge { + case .all: edgePrefix = "inset" + case .top: edgePrefix = "top" + case .leading: edgePrefix = "left" + case .trailing: edgePrefix = "right" + case .bottom: edgePrefix = "bottom" + case .horizontal: edgePrefix = "inset-x" + case .vertical: edgePrefix = "inset-y" + } + + if offset < 0 { + classes.append("-\(edgePrefix)-\(abs(offset))") + } else { + classes.append("\(edgePrefix)-\(offset)") + } + } + } + + return classes + } + + /// Shared instance for use across the framework + public static let shared = PositionStyleOperation() + + /// Private initializer to enforce singleton usage + private init() {} +} + +// Extension for Element to provide position styling +extension Element { + /// Applies positioning styling to the element with one or more edges. + /// + /// Sets the position type and optional inset values for specified edges, scoped to modifiers if provided. + /// + /// - Parameters: + /// - type: Specifies the positioning method (e.g., absolute, fixed). + /// - edges: One or more edges to apply the inset to. Defaults to `.all`. + /// - length: The distance value for the specified edges (e.g., "0", "4", "auto"). + /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. + /// - Returns: A new element with updated position classes. + public func position( + _ type: PositionType? = nil, + at edges: Edge..., + offset length: Int? = nil, + on modifiers: Modifier... + ) -> Element { + let params = PositionStyleOperation.Parameters( + type: type, + edges: edges, + offset: length + ) + + return PositionStyleOperation.shared.applyToElement( + self, + params: params, + modifiers: modifiers + ) + } +} + +// Extension for ResponsiveBuilder to provide position styling +extension ResponsiveBuilder { + /// Applies position styling in a responsive context. + /// + /// - Parameters: + /// - type: The position type. + /// - edges: The edges to apply positioning to. + /// - offset: The offset value for positioning. + /// - Returns: The builder for method chaining. + @discardableResult + public func position( + _ type: PositionType? = nil, + at edges: Edge..., + offset: Int? = nil + ) -> ResponsiveBuilder { + let params = PositionStyleOperation.Parameters( + type: type, + edges: edges, + offset: offset + ) + + return PositionStyleOperation.shared.applyToBuilder(self, params: params) + } +} + +// Global function for Declarative DSL +/// Applies position styling in the responsive context. +/// +/// - Parameters: +/// - type: The position type. +/// - edges: The edges to apply positioning to. +/// - offset: The offset value for positioning. +/// - Returns: A responsive modification for positioning. +public func position( + _ type: PositionType? = nil, + at edges: Edge..., + offset: Int? = nil +) -> ResponsiveModification { + let params = PositionStyleOperation.Parameters( + type: type, + edges: edges, + offset: offset + ) + + return PositionStyleOperation.shared.asModification(params: params) +} diff --git a/Sources/WebUI/Styles/Positioning/Position/PositionTypes.swift b/Sources/WebUI/Styles/Positioning/Position/PositionTypes.swift new file mode 100644 index 00000000..ebd01ebf --- /dev/null +++ b/Sources/WebUI/Styles/Positioning/Position/PositionTypes.swift @@ -0,0 +1,15 @@ +/// Defines positioning types for elements. +/// +/// Specifies how an element is positioned within its parent or viewport. +public enum PositionType: String { + /// Positions the element using the normal document flow. + case `static` + /// Positions the element relative to its normal position. + case relative + /// Positions the element relative to its nearest positioned ancestor. + case absolute + /// Positions the element relative to the viewport and fixes it in place. + case fixed + /// Positions the element relative to its normal position and sticks on scroll. + case sticky +} diff --git a/Sources/WebUI/Styles/Positioning/Scroll.swift b/Sources/WebUI/Styles/Positioning/Scroll.swift deleted file mode 100644 index e4bea2e4..00000000 --- a/Sources/WebUI/Styles/Positioning/Scroll.swift +++ /dev/null @@ -1,148 +0,0 @@ -/// Defines scroll behavior for an element. -/// -/// Specifies how scrolling behaves, either smoothly or instantly. -public enum ScrollBehavior: String { - /// Smooth scrolling behavior - case smooth = "smooth" - /// Instant scrolling behavior - case auto = "auto" -} - -/// Defines scroll snap alignment options. -/// -/// Specifies how an element aligns within a scroll container. -public enum ScrollSnapAlign: String { - /// Aligns to the start of the snap container - case start - /// Aligns to the end of the snap container - case end - /// Aligns to the center of the snap container - case center -} - -/// Defines scroll snap stop behavior. -/// -/// Controls whether scrolling can skip past snap points. -public enum ScrollSnapStop: String { - /// Allows scrolling to skip snap points - case normal = "normal" - /// Forces scrolling to stop at snap points - case always = "always" -} - -/// Defines scroll snap type options. -/// -/// Specifies the type and strictness of scroll snapping. -public enum ScrollSnapType: String { - /// Snap along the x-axis - case x = "x" - /// Snap along the y-axis - case y = "y" - /// Snap along both axes - case both = "both" - /// Mandatory snapping with stricter enforcement - case mandatory = "mandatory" - /// Proximity snapping with looser enforcement - case proximity = "proximity" -} - -extension Element { - /// Applies scroll-related styles to the element. - /// - /// Adds Tailwind CSS classes for scroll behavior, margin, padding, and snap properties. - /// - /// - Parameters: - /// - behavior: Sets the scroll behavior (smooth or auto). - /// - margin: Sets the scroll margin in 0.25rem increments for all sides or specific edges. - /// - padding: Sets the scroll padding in 0.25rem increments for all sides or specific edges. - /// - snapAlign: Sets the scroll snap alignment (start, end, center). - /// - snapStop: Sets the scroll snap stop behavior (normal, always). - /// - snapType: Sets the scroll snap type (x, y, both, mandatory, proximity). - /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. - /// - Returns: A new element with updated scroll-related classes. - public func scroll( - behavior: ScrollBehavior? = nil, - margin: (value: Int, edges: [Edge])? = nil, - padding: (value: Int, edges: [Edge])? = nil, - snapAlign: ScrollSnapAlign? = nil, - snapStop: ScrollSnapStop? = nil, - snapType: ScrollSnapType? = nil, - on modifiers: Modifier... - ) -> Element { - var baseClasses: [String] = [] - - // Scroll Behavior - if let behaviorValue = behavior { - baseClasses.append("scroll-\(behaviorValue.rawValue)") - } - - // Scroll Margin - if let (value, edges) = margin { - let effectiveEdges = edges.isEmpty ? [Edge.all] : edges - baseClasses.append( - contentsOf: effectiveEdges.map { edge in - let edgePrefix: String - switch edge { - case .all: edgePrefix = "" - case .top: edgePrefix = "t" - case .bottom: edgePrefix = "b" - case .leading: edgePrefix = "s" - case .trailing: edgePrefix = "e" - case .horizontal: edgePrefix = "x" - case .vertical: edgePrefix = "y" - } - - return "scroll-m\(edgePrefix.isEmpty ? "" : "\(edgePrefix)")-\(value)" - } - ) - } - - // Scroll Padding - if let (value, edges) = padding { - let effectiveEdges = edges.isEmpty ? [Edge.all] : edges - baseClasses.append( - contentsOf: effectiveEdges.map { edge in - let edgePrefix: String - switch edge { - case .all: edgePrefix = "" - case .top: edgePrefix = "t" - case .bottom: edgePrefix = "b" - case .leading: edgePrefix = "s" - case .trailing: edgePrefix = "e" - case .horizontal: edgePrefix = "x" - case .vertical: edgePrefix = "y" - } - return "scroll-p\(edgePrefix.isEmpty ? "" : "\(edgePrefix)")-\(value)" - } - ) - } - - // Scroll Snap Align - if let snapAlignValue = snapAlign { - baseClasses.append("snap-\(snapAlignValue.rawValue)") - } - - // Scroll Snap Stop - if let snapStopValue = snapStop { - baseClasses.append("snap-\(snapStopValue.rawValue)") - } - - // Scroll Snap Type - if let snapTypeValue = snapType { - baseClasses.append("snap-\(snapTypeValue.rawValue)") - } - - let newClasses = combineClasses(baseClasses, withModifiers: modifiers) - - return Element( - tag: self.tag, - id: self.id, - classes: (self.classes ?? []) + newClasses, - role: self.role, - label: self.label, - isSelfClosing: self.isSelfClosing, - customAttributes: self.customAttributes, - content: self.contentBuilder - ) - } -} diff --git a/Sources/WebUI/Styles/Positioning/Scroll/ScrollStyleOperation.swift b/Sources/WebUI/Styles/Positioning/Scroll/ScrollStyleOperation.swift new file mode 100644 index 00000000..3bec1a62 --- /dev/null +++ b/Sources/WebUI/Styles/Positioning/Scroll/ScrollStyleOperation.swift @@ -0,0 +1,248 @@ +import Foundation + +/// Style operation for scroll styling +/// +/// Provides a unified implementation for scroll styling that can be used across +/// Element methods and the Declarative DSL functions. +public struct ScrollStyleOperation: StyleOperation, @unchecked Sendable { + /// Parameters for scroll styling + public struct Parameters { + /// The scroll behavior + public let behavior: ScrollBehavior? + + /// The scroll margin value and edges + public let margin: (value: Int, edges: [Edge])? + + /// The scroll padding value and edges + public let padding: (value: Int, edges: [Edge])? + + /// The scroll snap alignment + public let snapAlign: ScrollSnapAlign? + + /// The scroll snap stop behavior + public let snapStop: ScrollSnapStop? + + /// The scroll snap type + public let snapType: ScrollSnapType? + + /// Creates parameters for scroll styling + /// + /// - Parameters: + /// - behavior: The scroll behavior + /// - margin: The scroll margin value and edges + /// - padding: The scroll padding value and edges + /// - snapAlign: The scroll snap alignment + /// - snapStop: The scroll snap stop behavior + /// - snapType: The scroll snap type + public init( + behavior: ScrollBehavior? = nil, + margin: (value: Int, edges: [Edge])? = nil, + padding: (value: Int, edges: [Edge])? = nil, + snapAlign: ScrollSnapAlign? = nil, + snapStop: ScrollSnapStop? = nil, + snapType: ScrollSnapType? = nil + ) { + self.behavior = behavior + self.margin = margin + self.padding = padding + self.snapAlign = snapAlign + self.snapStop = snapStop + self.snapType = snapType + } + + /// Creates parameters from a StyleParameters container + /// + /// - Parameter params: The style parameters container + /// - Returns: ScrollStyleOperation.Parameters + public static func from(_ params: StyleParameters) -> Parameters { + Parameters( + behavior: params.get("behavior"), + margin: params.get("margin"), + padding: params.get("padding"), + snapAlign: params.get("snapAlign"), + snapStop: params.get("snapStop"), + snapType: params.get("snapType") + ) + } + } + + /// Applies the scroll style and returns the appropriate CSS classes + /// + /// - Parameter params: The parameters for scroll styling + /// - Returns: An array of CSS class names to be applied to elements + public func applyClasses(params: Parameters) -> [String] { + var classes: [String] = [] + + // Scroll Behavior + if let behavior = params.behavior { + classes.append("scroll-\(behavior.rawValue)") + } + + // Scroll Margin + if let (value, edges) = params.margin { + let effectiveEdges = edges.isEmpty ? [Edge.all] : edges + for edge in effectiveEdges { + let edgePrefix: String + switch edge { + case .all: edgePrefix = "" + case .top: edgePrefix = "t" + case .bottom: edgePrefix = "b" + case .leading: edgePrefix = "s" + case .trailing: edgePrefix = "e" + case .horizontal: edgePrefix = "x" + case .vertical: edgePrefix = "y" + } + + classes.append("scroll-m\(edgePrefix.isEmpty ? "" : "\(edgePrefix)")-\(value)") + } + } + + // Scroll Padding + if let (value, edges) = params.padding { + let effectiveEdges = edges.isEmpty ? [Edge.all] : edges + for edge in effectiveEdges { + let edgePrefix: String + switch edge { + case .all: edgePrefix = "" + case .top: edgePrefix = "t" + case .bottom: edgePrefix = "b" + case .leading: edgePrefix = "s" + case .trailing: edgePrefix = "e" + case .horizontal: edgePrefix = "x" + case .vertical: edgePrefix = "y" + } + + classes.append("scroll-p\(edgePrefix.isEmpty ? "" : "\(edgePrefix)")-\(value)") + } + } + + // Scroll Snap Align + if let snapAlign = params.snapAlign { + classes.append("snap-\(snapAlign.rawValue)") + } + + // Scroll Snap Stop + if let snapStop = params.snapStop { + classes.append("snap-\(snapStop.rawValue)") + } + + // Scroll Snap Type + if let snapType = params.snapType { + classes.append("snap-\(snapType.rawValue)") + } + + return classes + } + + /// Shared instance for use across the framework + public static let shared = ScrollStyleOperation() + + /// Private initializer to enforce singleton usage + private init() {} +} + +// Extension for Element to provide scroll styling +extension Element { + /// Applies scroll-related styles to the element. + /// + /// Adds Tailwind CSS classes for scroll behavior, margin, padding, and snap properties. + /// + /// - Parameters: + /// - behavior: Sets the scroll behavior (smooth or auto). + /// - margin: Sets the scroll margin in 0.25rem increments for all sides or specific edges. + /// - padding: Sets the scroll padding in 0.25rem increments for all sides or specific edges. + /// - snapAlign: Sets the scroll snap alignment (start, end, center). + /// - snapStop: Sets the scroll snap stop behavior (normal, always). + /// - snapType: Sets the scroll snap type (x, y, both, mandatory, proximity). + /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. + /// - Returns: A new element with updated scroll-related classes. + public func scroll( + behavior: ScrollBehavior? = nil, + margin: (value: Int, edges: [Edge])? = nil, + padding: (value: Int, edges: [Edge])? = nil, + snapAlign: ScrollSnapAlign? = nil, + snapStop: ScrollSnapStop? = nil, + snapType: ScrollSnapType? = nil, + on modifiers: Modifier... + ) -> Element { + let params = ScrollStyleOperation.Parameters( + behavior: behavior, + margin: margin, + padding: padding, + snapAlign: snapAlign, + snapStop: snapStop, + snapType: snapType + ) + + return ScrollStyleOperation.shared.applyToElement( + self, + params: params, + modifiers: modifiers + ) + } +} + +// Extension for ResponsiveBuilder to provide scroll styling +extension ResponsiveBuilder { + /// Applies scroll styling in a responsive context. + /// + /// - Parameters: + /// - behavior: The scroll behavior. + /// - margin: The scroll margin value and edges. + /// - padding: The scroll padding value and edges. + /// - snapAlign: The scroll snap alignment. + /// - snapStop: The scroll snap stop behavior. + /// - snapType: The scroll snap type. + /// - Returns: The builder for method chaining. + @discardableResult + public func scroll( + behavior: ScrollBehavior? = nil, + margin: (value: Int, edges: [Edge])? = nil, + padding: (value: Int, edges: [Edge])? = nil, + snapAlign: ScrollSnapAlign? = nil, + snapStop: ScrollSnapStop? = nil, + snapType: ScrollSnapType? = nil + ) -> ResponsiveBuilder { + let params = ScrollStyleOperation.Parameters( + behavior: behavior, + margin: margin, + padding: padding, + snapAlign: snapAlign, + snapStop: snapStop, + snapType: snapType + ) + + return ScrollStyleOperation.shared.applyToBuilder(self, params: params) + } +} + +// Global function for Declarative DSL +/// Applies scroll styling in the responsive context. +/// +/// - Parameters: +/// - behavior: The scroll behavior. +/// - margin: The scroll margin value and edges. +/// - padding: The scroll padding value and edges. +/// - snapAlign: The scroll snap alignment. +/// - snapStop: The scroll snap stop behavior. +/// - snapType: The scroll snap type. +/// - Returns: A responsive modification for scroll. +public func scroll( + behavior: ScrollBehavior? = nil, + margin: (value: Int, edges: [Edge])? = nil, + padding: (value: Int, edges: [Edge])? = nil, + snapAlign: ScrollSnapAlign? = nil, + snapStop: ScrollSnapStop? = nil, + snapType: ScrollSnapType? = nil +) -> ResponsiveModification { + let params = ScrollStyleOperation.Parameters( + behavior: behavior, + margin: margin, + padding: padding, + snapAlign: snapAlign, + snapStop: snapStop, + snapType: snapType + ) + + return ScrollStyleOperation.shared.asModification(params: params) +} diff --git a/Sources/WebUI/Styles/Positioning/Scroll/ScrollTypes.swift b/Sources/WebUI/Styles/Positioning/Scroll/ScrollTypes.swift new file mode 100644 index 00000000..d8d61a0e --- /dev/null +++ b/Sources/WebUI/Styles/Positioning/Scroll/ScrollTypes.swift @@ -0,0 +1,49 @@ +/// Defines scroll behavior for an element. +/// +/// Specifies how scrolling behaves, either smoothly or instantly. +public enum ScrollBehavior: String { + /// Smooth scrolling behavior + case smooth = "smooth" + /// Instant scrolling behavior + case auto = "auto" +} + +/// Defines scroll snap alignment options. +/// +/// Specifies how an element aligns within a scroll container. +public enum ScrollSnapAlign: String { + /// Aligns to the start of the snap container + case start + /// Aligns to the end of the snap container + case end + /// Aligns to the center of the snap container + case center +} + +/// Defines scroll snap stop behavior. +/// +/// Controls whether scrolling can skip past snap points. +public enum ScrollSnapStop: String { + /// Allows scrolling to skip snap points + case normal = "normal" + /// Forces scrolling to stop at snap points + case always = "always" +} + +/// Defines scroll snap type options. +/// +/// Specifies the type and strictness of scroll snapping. +public enum ScrollSnapType: String { + /// Snap along the x-axis + case x = "x" + /// Snap along the y-axis + case y = "y" + /// Snap along both axes + case both = "both" + /// Mandatory snapping with stricter enforcement + case mandatory = "mandatory" + /// Proximity snapping with looser enforcement + case proximity = "proximity" +} + +// Implementation has been moved to ScrollStyleOperation.swift diff --git a/Sources/WebUI/Styles/Positioning/Transform.swift b/Sources/WebUI/Styles/Positioning/Transform.swift deleted file mode 100644 index 1a6f1359..00000000 --- a/Sources/WebUI/Styles/Positioning/Transform.swift +++ /dev/null @@ -1,63 +0,0 @@ -extension Element { - /// Applies transformation styling to the element. - /// - /// Adds classes for scaling, rotating, translating, and skewing, optionally scoped to modifiers. - /// Transform values are provided as (x: Int?, y: Int?) tuples for individual axis control. - /// - /// - Parameters: - /// - scale: Sets scale factor(s) as an optional (x: Int?, y: Int?) tuple. - /// - rotate: Specifies the rotation angle in degrees. - /// - translate: Sets translation distance(s) as an optional (x: Int?, y: Int?) tuple. - /// - skew: Sets skew angle(s) as an optional (x: Int?, y: Int?) tuple. - /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. - /// - Returns: A new element with updated transform classes. - public func transform( - scale: (x: Int?, y: Int?)? = nil, - rotate: Int? = nil, - translate: (x: Int?, y: Int?)? = nil, - skew: (x: Int?, y: Int?)? = nil, - on modifiers: Modifier... - ) -> Element { - var baseClasses: [String] = ["transform"] - - if let scaleTuple = scale { - if let x = scaleTuple.x { baseClasses.append("scale-x-\(x)") } - if let y = scaleTuple.y { baseClasses.append("scale-y-\(y)") } - } - - if let rotate = rotate { - baseClasses.append(rotate < 0 ? "rotate-\(-rotate)" : "rotate-\(rotate)") - } - - if let translateTuple = translate { - if let x = translateTuple.x { - baseClasses.append(x < 0 ? "translate-x-\(-x)" : "translate-x-\(x)") - } - if let y = translateTuple.y { - baseClasses.append(y < 0 ? "translate-y-\(-y)" : "translate-y-\(y)") - } - } - - if let skewTuple = skew { - if let x = skewTuple.x { - baseClasses.append(x < 0 ? "skew-x-\(-x)" : "skew-x-\(x)") - } - if let y = skewTuple.y { - baseClasses.append(y < 0 ? "skew-y-\(-y)" : "skew-y-\(y)") - } - } - - let newClasses = combineClasses(baseClasses, withModifiers: modifiers) - - return Element( - tag: self.tag, - id: self.id, - classes: (self.classes ?? []) + newClasses, - role: self.role, - label: self.label, - isSelfClosing: self.isSelfClosing, - customAttributes: self.customAttributes, - content: self.contentBuilder - ) - } -} diff --git a/Sources/WebUI/Styles/Positioning/TransformStyleOperation.swift b/Sources/WebUI/Styles/Positioning/TransformStyleOperation.swift new file mode 100644 index 00000000..e2ac7d79 --- /dev/null +++ b/Sources/WebUI/Styles/Positioning/TransformStyleOperation.swift @@ -0,0 +1,186 @@ +import Foundation + +/// Style operation for transform styling +/// +/// Provides a unified implementation for transform styling that can be used across +/// Element methods and the Declarative DSL functions. +public struct TransformStyleOperation: StyleOperation, @unchecked Sendable { + /// Parameters for transform styling + public struct Parameters { + /// The scale values (x, y) + public let scale: (x: Int?, y: Int?)? + + /// The rotation angle in degrees + public let rotate: Int? + + /// The translation values (x, y) + public let translate: (x: Int?, y: Int?)? + + /// The skew values (x, y) + public let skew: (x: Int?, y: Int?)? + + /// Creates parameters for transform styling + /// + /// - Parameters: + /// - scale: The scale values (x, y) + /// - rotate: The rotation angle in degrees + /// - translate: The translation values (x, y) + /// - skew: The skew values (x, y) + public init( + scale: (x: Int?, y: Int?)? = nil, + rotate: Int? = nil, + translate: (x: Int?, y: Int?)? = nil, + skew: (x: Int?, y: Int?)? = nil + ) { + self.scale = scale + self.rotate = rotate + self.translate = translate + self.skew = skew + } + + /// Creates parameters from a StyleParameters container + /// + /// - Parameter params: The style parameters container + /// - Returns: TransformStyleOperation.Parameters + public static func from(_ params: StyleParameters) -> Parameters { + Parameters( + scale: params.get("scale"), + rotate: params.get("rotate"), + translate: params.get("translate"), + skew: params.get("skew") + ) + } + } + + /// Applies the transform style and returns the appropriate CSS classes + /// + /// - Parameter params: The parameters for transform styling + /// - Returns: An array of CSS class names to be applied to elements + public func applyClasses(params: Parameters) -> [String] { + var classes: [String] = ["transform"] + + if let scaleTuple = params.scale { + if let x = scaleTuple.x { classes.append("scale-x-\(x)") } + if let y = scaleTuple.y { classes.append("scale-y-\(y)") } + } + + if let rotate = params.rotate { + classes.append(rotate < 0 ? "-rotate-\(-rotate)" : "rotate-\(rotate)") + } + + if let translateTuple = params.translate { + if let x = translateTuple.x { + classes.append(x < 0 ? "-translate-x-\(-x)" : "translate-x-\(x)") + } + if let y = translateTuple.y { + classes.append(y < 0 ? "-translate-y-\(-y)" : "translate-y-\(y)") + } + } + + if let skewTuple = params.skew { + if let x = skewTuple.x { + classes.append(x < 0 ? "-skew-x-\(-x)" : "skew-x-\(x)") + } + if let y = skewTuple.y { + classes.append(y < 0 ? "-skew-y-\(-y)" : "skew-y-\(y)") + } + } + + return classes + } + + /// Shared instance for use across the framework + public static let shared = TransformStyleOperation() + + /// Private initializer to enforce singleton usage + private init() {} +} + +// Extension for Element to provide transform styling +extension Element { + /// Applies transformation styling to the element. + /// + /// Adds classes for scaling, rotating, translating, and skewing, optionally scoped to modifiers. + /// Transform values are provided as (x: Int?, y: Int?) tuples for individual axis control. + /// + /// - Parameters: + /// - scale: Sets scale factor(s) as an optional (x: Int?, y: Int?) tuple. + /// - rotate: Specifies the rotation angle in degrees. + /// - translate: Sets translation distance(s) as an optional (x: Int?, y: Int?) tuple. + /// - skew: Sets skew angle(s) as an optional (x: Int?, y: Int?) tuple. + /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. + /// - Returns: A new element with updated transform classes. + public func transform( + scale: (x: Int?, y: Int?)? = nil, + rotate: Int? = nil, + translate: (x: Int?, y: Int?)? = nil, + skew: (x: Int?, y: Int?)? = nil, + on modifiers: Modifier... + ) -> Element { + let params = TransformStyleOperation.Parameters( + scale: scale, + rotate: rotate, + translate: translate, + skew: skew + ) + + return TransformStyleOperation.shared.applyToElement( + self, + params: params, + modifiers: modifiers + ) + } +} + +// Extension for ResponsiveBuilder to provide transform styling +extension ResponsiveBuilder { + /// Applies transform styling in a responsive context. + /// + /// - Parameters: + /// - scale: The scale values (x, y). + /// - rotate: The rotation angle in degrees. + /// - translate: The translation values (x, y). + /// - skew: The skew values (x, y). + /// - Returns: The builder for method chaining. + @discardableResult + public func transform( + scale: (x: Int?, y: Int?)? = nil, + rotate: Int? = nil, + translate: (x: Int?, y: Int?)? = nil, + skew: (x: Int?, y: Int?)? = nil + ) -> ResponsiveBuilder { + let params = TransformStyleOperation.Parameters( + scale: scale, + rotate: rotate, + translate: translate, + skew: skew + ) + + return TransformStyleOperation.shared.applyToBuilder(self, params: params) + } +} + +// Global function for Declarative DSL +/// Applies transform styling in the responsive context. +/// +/// - Parameters: +/// - scale: The scale values (x, y). +/// - rotate: The rotation angle in degrees. +/// - translate: The translation values (x, y). +/// - skew: The skew values (x, y). +/// - Returns: A responsive modification for transform. +public func transform( + scale: (x: Int?, y: Int?)? = nil, + rotate: Int? = nil, + translate: (x: Int?, y: Int?)? = nil, + skew: (x: Int?, y: Int?)? = nil +) -> ResponsiveModification { + let params = TransformStyleOperation.Parameters( + scale: scale, + rotate: rotate, + translate: translate, + skew: skew + ) + + return TransformStyleOperation.shared.asModification(params: params) +} diff --git a/Sources/WebUI/Styles/Positioning/Transition.swift b/Sources/WebUI/Styles/Positioning/Transition.swift deleted file mode 100644 index 5a5c5645..00000000 --- a/Sources/WebUI/Styles/Positioning/Transition.swift +++ /dev/null @@ -1,71 +0,0 @@ -/// Defines properties for CSS transitions. -/// -/// Specifies which element properties are animated during transitions. -public enum TransitionProperty: String { - /// Transitions all animatable properties. - case all - /// Transitions color-related properties. - case colors - /// Transitions opacity. - case opacity - /// Transitions box shadow. - case shadow - /// Transitions transform properties. - case transform -} - -/// Defines easing functions for transitions. -/// -/// Specifies the timing curve for transition animations. -public enum Easing: String { - /// Applies a linear timing function. - case linear - /// Applies an ease-in timing function. - case `in` - /// Applies an ease-out timing function. - case out - /// Applies an ease-in-out timing function. - case inOut = "in-out" -} - -extension Element { - /// Applies transition styling to the element. - /// - /// Adds classes for animating properties with duration, easing, and delay. - /// - /// - Parameters: - /// - property: Specifies the property to animate (defaults to all if nil). - /// - duration: Sets the transition duration in milliseconds. - /// - easing: Defines the timing function for the transition. - /// - delay: Sets the delay before the transition starts in milliseconds. - /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. - /// - Returns: A new element with updated transition classes. - public func transition( - of property: TransitionProperty? = nil, - for duration: Int? = nil, - easing: Easing? = nil, - delay: Int? = nil, - on modifiers: Modifier... - ) -> Element { - var baseClasses: [String] = [] - baseClasses.append( - property != nil ? "transition-\(property!.rawValue)" : "transition" - ) - if let duration = duration { baseClasses.append("duration-\(duration)") } - if let easing = easing { baseClasses.append("ease-\(easing.rawValue)") } - if let delay = delay { baseClasses.append("delay-\(delay)") } - - let newClasses = combineClasses(baseClasses, withModifiers: modifiers) - - return Element( - tag: self.tag, - id: self.id, - classes: (self.classes ?? []) + newClasses, - role: self.role, - label: self.label, - isSelfClosing: self.isSelfClosing, - customAttributes: self.customAttributes, - content: self.contentBuilder - ) - } -} diff --git a/Sources/WebUI/Styles/Positioning/Transition/TransitionStyleOperation.swift b/Sources/WebUI/Styles/Positioning/Transition/TransitionStyleOperation.swift new file mode 100644 index 00000000..088dd9d4 --- /dev/null +++ b/Sources/WebUI/Styles/Positioning/Transition/TransitionStyleOperation.swift @@ -0,0 +1,176 @@ +import Foundation + +/// Style operation for transition styling +/// +/// Provides a unified implementation for transition styling that can be used across +/// Element methods and the Declarative DSL functions. +public struct TransitionStyleOperation: StyleOperation, @unchecked Sendable { + /// Parameters for transition styling + public struct Parameters { + /// The transition property + public let property: TransitionProperty? + + /// The transition duration in milliseconds + public let duration: Int? + + /// The transition easing function + public let easing: Easing? + + /// The transition delay in milliseconds + public let delay: Int? + + /// Creates parameters for transition styling + /// + /// - Parameters: + /// - property: The transition property + /// - duration: The transition duration in milliseconds + /// - easing: The transition easing function + /// - delay: The transition delay in milliseconds + public init( + property: TransitionProperty? = nil, + duration: Int? = nil, + easing: Easing? = nil, + delay: Int? = nil + ) { + self.property = property + self.duration = duration + self.easing = easing + self.delay = delay + } + + /// Creates parameters from a StyleParameters container + /// + /// - Parameter params: The style parameters container + /// - Returns: TransitionStyleOperation.Parameters + public static func from(_ params: StyleParameters) -> Parameters { + Parameters( + property: params.get("property"), + duration: params.get("duration"), + easing: params.get("easing"), + delay: params.get("delay") + ) + } + } + + /// Applies the transition style and returns the appropriate CSS classes + /// + /// - Parameter params: The parameters for transition styling + /// - Returns: An array of CSS class names to be applied to elements + public func applyClasses(params: Parameters) -> [String] { + var classes: [String] = [] + + if let property = params.property { + classes.append("transition-\(property.rawValue)") + } else { + classes.append("transition") + } + + if let duration = params.duration { + classes.append("duration-\(duration)") + } + + if let easing = params.easing { + classes.append("ease-\(easing.rawValue)") + } + + if let delay = params.delay { + classes.append("delay-\(delay)") + } + + return classes + } + + /// Shared instance for use across the framework + public static let shared = TransitionStyleOperation() + + /// Private initializer to enforce singleton usage + private init() {} +} + +// Extension for Element to provide transition styling +extension Element { + /// Applies transition styling to the element. + /// + /// Adds classes for animating properties with duration, easing, and delay. + /// + /// - Parameters: + /// - property: Specifies the property to animate (defaults to all if nil). + /// - duration: Sets the transition duration in milliseconds. + /// - easing: Defines the timing function for the transition. + /// - delay: Sets the delay before the transition starts in milliseconds. + /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. + /// - Returns: A new element with updated transition classes. + public func transition( + of property: TransitionProperty? = nil, + for duration: Int? = nil, + easing: Easing? = nil, + delay: Int? = nil, + on modifiers: Modifier... + ) -> Element { + let params = TransitionStyleOperation.Parameters( + property: property, + duration: duration, + easing: easing, + delay: delay + ) + + return TransitionStyleOperation.shared.applyToElement( + self, + params: params, + modifiers: modifiers + ) + } +} + +// Extension for ResponsiveBuilder to provide transition styling +extension ResponsiveBuilder { + /// Applies transition styling in a responsive context. + /// + /// - Parameters: + /// - property: The transition property. + /// - duration: The transition duration in milliseconds. + /// - easing: The transition easing function. + /// - delay: The transition delay in milliseconds. + /// - Returns: The builder for method chaining. + @discardableResult + public func transition( + of property: TransitionProperty? = nil, + for duration: Int? = nil, + easing: Easing? = nil, + delay: Int? = nil + ) -> ResponsiveBuilder { + let params = TransitionStyleOperation.Parameters( + property: property, + duration: duration, + easing: easing, + delay: delay + ) + + return TransitionStyleOperation.shared.applyToBuilder(self, params: params) + } +} + +// Global function for Declarative DSL +/// Applies transition styling in the responsive context. +/// +/// - Parameters: +/// - property: The transition property. +/// - duration: The transition duration in milliseconds. +/// - easing: The transition easing function. +/// - delay: The transition delay in milliseconds. +/// - Returns: A responsive modification for transition. +public func transition( + of property: TransitionProperty? = nil, + for duration: Int? = nil, + easing: Easing? = nil, + delay: Int? = nil +) -> ResponsiveModification { + let params = TransitionStyleOperation.Parameters( + property: property, + duration: duration, + easing: easing, + delay: delay + ) + + return TransitionStyleOperation.shared.asModification(params: params) +} diff --git a/Sources/WebUI/Styles/Positioning/Transition/TransitionTypes.swift b/Sources/WebUI/Styles/Positioning/Transition/TransitionTypes.swift new file mode 100644 index 00000000..5599d45f --- /dev/null +++ b/Sources/WebUI/Styles/Positioning/Transition/TransitionTypes.swift @@ -0,0 +1,31 @@ +/// Defines properties for CSS transitions. +/// +/// Specifies which element properties are animated during transitions. +public enum TransitionProperty: String { + /// Transitions all animatable properties. + case all + /// Transitions color-related properties. + case colors + /// Transitions opacity. + case opacity + /// Transitions box shadow. + case shadow + /// Transitions transform properties. + case transform +} + +/// Defines easing functions for transitions. +/// +/// Specifies the timing curve for transition animations. +public enum Easing: String { + /// Applies a linear timing function. + case linear + /// Applies an ease-in timing function. + case `in` + /// Applies an ease-out timing function. + case out + /// Applies an ease-in-out timing function. + case inOut = "in-out" +} + +// Implementation has been moved to TransitionStyleOperation.swift diff --git a/Sources/WebUI/Styles/Positioning/ZIndex.swift b/Sources/WebUI/Styles/Positioning/ZIndex.swift deleted file mode 100644 index 46e8e709..00000000 --- a/Sources/WebUI/Styles/Positioning/ZIndex.swift +++ /dev/null @@ -1,39 +0,0 @@ -extension Element { - /// Applies a z-index to the element. - /// - /// Sets the stacking order of the element, optionally scoped to modifiers. - /// - /// - Parameters: - /// - value: Specifies the z-index value as an integer. - /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. - /// - Returns: A new element with updated z-index classes. - /// - /// ## Example - /// ```swift - /// // Header that stays on top of other content - /// Header() - /// .zIndex(50) - /// - /// // Dropdown menu that appears above other elements when activated - /// Stack(classes: ["dropdown-menu"]) - /// .zIndex(10, on: .hover) - /// ``` - public func zIndex( - _ value: Int, - on modifiers: Modifier... - ) -> Element { - let baseClass = "z-\(value)" - let newClasses = combineClasses([baseClass], withModifiers: modifiers) - - return Element( - tag: self.tag, - id: self.id, - classes: (self.classes ?? []) + newClasses, - role: self.role, - label: self.label, - isSelfClosing: self.isSelfClosing, - customAttributes: self.customAttributes, - content: self.contentBuilder - ) - } -} diff --git a/Sources/WebUI/Styles/Positioning/ZIndexStyleOperation.swift b/Sources/WebUI/Styles/Positioning/ZIndexStyleOperation.swift new file mode 100644 index 00000000..a6c06c1d --- /dev/null +++ b/Sources/WebUI/Styles/Positioning/ZIndexStyleOperation.swift @@ -0,0 +1,104 @@ +import Foundation + +/// Style operation for z-index styling +/// +/// Provides a unified implementation for z-index styling that can be used across +/// Element methods and the Declarative DSL functions. +public struct ZIndexStyleOperation: StyleOperation, @unchecked Sendable { + /// Parameters for z-index styling + public struct Parameters { + /// The z-index value + public let value: Int + + /// Creates parameters for z-index styling + /// + /// - Parameter value: The z-index value + public init(value: Int) { + self.value = value + } + + /// Creates parameters from a StyleParameters container + /// + /// - Parameter params: The style parameters container + /// - Returns: ZIndexStyleOperation.Parameters + public static func from(_ params: StyleParameters) -> Parameters { + Parameters( + value: params.get("value")! + ) + } + } + + /// Applies the z-index style and returns the appropriate CSS classes + /// + /// - Parameter params: The parameters for z-index styling + /// - Returns: An array of CSS class names to be applied to elements + public func applyClasses(params: Parameters) -> [String] { + ["z-\(params.value)"] + } + + /// Shared instance for use across the framework + public static let shared = ZIndexStyleOperation() + + /// Private initializer to enforce singleton usage + private init() {} +} + +// Extension for Element to provide z-index styling +extension Element { + /// Applies a z-index to the element. + /// + /// Sets the stacking order of the element, optionally scoped to modifiers. + /// + /// - Parameters: + /// - value: Specifies the z-index value as an integer. + /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. + /// - Returns: A new element with updated z-index classes. + /// + /// ## Example + /// ```swift + /// // Header that stays on top of other content + /// Header() + /// .zIndex(50) + /// + /// // Dropdown menu that appears above other elements when activated + /// Stack(classes: ["dropdown-menu"]) + /// .zIndex(10, on: .hover) + /// ``` + public func zIndex( + _ value: Int, + on modifiers: Modifier... + ) -> Element { + let params = ZIndexStyleOperation.Parameters(value: value) + + return ZIndexStyleOperation.shared.applyToElement( + self, + params: params, + modifiers: modifiers + ) + } +} + +// Extension for ResponsiveBuilder to provide z-index styling +extension ResponsiveBuilder { + /// Applies z-index styling in a responsive context. + /// + /// - Parameter value: The z-index value. + /// - Returns: The builder for method chaining. + @discardableResult + public func zIndex(_ value: Int) -> ResponsiveBuilder { + let params = ZIndexStyleOperation.Parameters(value: value) + + return ZIndexStyleOperation.shared.applyToBuilder(self, params: params) + } +} + +// Global function for Declarative DSL +/// Applies z-index styling in the responsive context. +/// +/// - Parameter value: The z-index value. +/// - Returns: A responsive modification for z-index. +public func zIndex(_ value: Int) -> ResponsiveModification { + let params = ZIndexStyleOperation.Parameters(value: value) + + return ZIndexStyleOperation.shared.asModification(params: params) +} diff --git a/Sources/WebUIMarkdown/WebUIMarkdown.swift b/Sources/WebUIMarkdown/WebUIMarkdown.swift index 179a74dc..5c1c9d0d 100644 --- a/Sources/WebUIMarkdown/WebUIMarkdown.swift +++ b/Sources/WebUIMarkdown/WebUIMarkdown.swift @@ -308,10 +308,10 @@ public struct HtmlRenderer: MarkupWalker { public mutating func visitCodeBlock(_ codeBlock: CodeBlock) { let language = codeBlock.language ?? "" logger.trace("Rendering code block with language: \(language)") - + // Simply extract the code content let code = escapeHTML(codeBlock.code) - + // Build the basic HTML for the code block html += "
"
         html += code
@@ -450,12 +450,11 @@ public struct HtmlRenderer: MarkupWalker {
             .replacingOccurrences(of: "\"", with: """)
             .replacingOccurrences(of: "'", with: "'")
     }
-    
+
     /// Unescapes HTML span tags used for syntax highlighting while keeping other HTML escaping intact
     /// - Parameter string: The string containing escaped HTML span tags
     /// - Returns: A string with span tags unescaped but other HTML escaping intact
 
-
     /// Extracts a filename from the first line of the code block if it matches a comment convention for the language.
     ///
     /// - Parameters:
@@ -479,8 +478,13 @@ public struct HtmlRenderer: MarkupWalker {
             pattern = ""
         }
         if let regex = try? NSRegularExpression(pattern: pattern, options: []),
-           let match = regex.firstMatch(in: trimmed, options: [], range: NSRange(location: 0, length: trimmed.utf16.count)),
-           let range = Range(match.range(at: 1), in: trimmed) {
+            let match = regex.firstMatch(
+                in: trimmed,
+                options: [],
+                range: NSRange(location: 0, length: trimmed.utf16.count)
+            ),
+            let range = Range(match.range(at: 1), in: trimmed)
+        {
             filename = String(trimmed[range])
             let codeWithoutFilename = lines.dropFirst().joined(separator: "\n")
             return (filename, codeWithoutFilename)
@@ -488,7 +492,5 @@ public struct HtmlRenderer: MarkupWalker {
         return (nil, code)
     }
 
-
-    
     // Removed syntax highlighting functions
 }
diff --git a/Tests/WebUIMarkdown/HighlightingTests.swift b/Tests/WebUIMarkdown/HighlightingTests.swift
index 6133328b..2f2bc1b2 100644
--- a/Tests/WebUIMarkdown/HighlightingTests.swift
+++ b/Tests/WebUIMarkdown/HighlightingTests.swift
@@ -24,30 +24,30 @@ struct HighlightingTests {
     @Test("Shell highlighting")
     func testShellHighlighting() {
         let code = """
-        # comment
-        if [ $FOO -eq 1 ]; then
-          echo "hello" && exit 0
-        fi
-        """
+            # comment
+            if [ $FOO -eq 1 ]; then
+              echo "hello" && exit 0
+            fi
+            """
         let html = renderHTML(markdown: code, language: "sh")
         #expect(html.contains("hl-comment"))
         #expect(html.contains("hl-keyword"))
         #expect(html.contains("hl-variable"))
         #expect(html.contains("hl-number"))
         #expect(html.contains("hl-operator"))
-        #expect(!html.contains("hl-literal")) // no literal in this code
+        #expect(!html.contains("hl-literal"))  // no literal in this code
     }
 
     @Test("Swift highlighting")
     func testSwiftHighlighting() {
         let code = """
-        // comment
-        let x: Int = 42
-        func greet(name: String) -> String {
-            print("Hello, \\(name)")
-            return "ok"
-        }
-        """
+            // comment
+            let x: Int = 42
+            func greet(name: String) -> String {
+                print("Hello, \\(name)")
+                return "ok"
+            }
+            """
         let html = renderHTML(markdown: code, language: "swift")
         #expect(html.contains("hl-comment"))
         #expect(html.contains("hl-keyword"))
@@ -61,11 +61,11 @@ struct HighlightingTests {
     @Test("YAML highlighting")
     func testYAMLHighlighting() {
         let code = """
-        # comment
-        name: "John"
-        age: 30
-        active: true
-        """
+            # comment
+            name: "John"
+            age: 30
+            active: true
+            """
         let html = renderHTML(markdown: code, language: "yml")
         #expect(html.contains("hl-comment"))
         #expect(html.contains("hl-attribute"))
@@ -90,4 +90,4 @@ struct HighlightingTests {
         #expect(!html.contains("hl-keyword"))
         #expect(!html.contains("hl-comment"))
     }
-}
\ No newline at end of file
+}
diff --git a/Tests/WebUITests/Styles/AppearanceTests.swift b/Tests/WebUITests/Styles/AppearanceTests.swift
index 988466d6..518fd81e 100644
--- a/Tests/WebUITests/Styles/AppearanceTests.swift
+++ b/Tests/WebUITests/Styles/AppearanceTests.swift
@@ -120,14 +120,14 @@ import Testing
 
     @Test("Shadow with size")
     func testShadowWithSize() async throws {
-        let element = Element(tag: "div").shadow(of: .lg)
+        let element = Element(tag: "div").shadow(size: .lg)
         let rendered = element.render()
         #expect(rendered.contains("class=\"shadow-lg\""))
     }
 
     @Test("Shadow with color and modifier")
     func testShadowWithColorAndModifier() async throws {
-        let element = Element(tag: "div").shadow(of: .md, color: .gray(._500), on: .hover)
+        let element = Element(tag: "div").shadow(size: .md, color: .gray(._500), on: .hover)
         let rendered = element.render()
         #expect(rendered.contains("class=\"hover:shadow-md hover:shadow-gray-500\""))
     }
@@ -143,7 +143,7 @@ import Testing
 
     @Test("Ring with color and modifier")
     func testRingWithColorAndModifier() async throws {
-        let element = Element(tag: "div").ring(of: 2, color: .pink(._400), on: .focus)
+        let element = Element(tag: "div").ring(size: 2, color: .pink(._400), on: .focus)
         let rendered = element.render()
         #expect(rendered.contains("class=\"focus:ring-2 focus:ring-pink-400\""))
     }
@@ -171,22 +171,6 @@ import Testing
         #expect(rendered.contains("class=\"md:flex md:flex-col\""))
     }
 
-    // MARK: - Grid Tests
-
-    @Test("Grid with columns and justify")
-    func testGridWithColumnsAndJustify() async throws {
-        let element = Element(tag: "div").grid(justify: .center, columns: 3)
-        let rendered = element.render()
-        #expect(rendered.contains("class=\"grid justify-center grid-cols-3\""))
-    }
-
-    @Test("Grid with align and modifier")
-    func testGridWithAlignAndModifier() async throws {
-        let element = Element(tag: "div").grid(align: .stretch, on: .lg)
-        let rendered = element.render()
-        #expect(rendered.contains("class=\"lg:grid lg:items-stretch\""))
-    }
-
     // MARK: - Hidden Tests
 
     @Test("Hidden element")
@@ -240,13 +224,12 @@ import Testing
         let element = Element(tag: "div")
             .background(color: .blue(._600))
             .border(of: 1, style: .solid, color: .blue(._800))
-            .shadow(of: .md)
             .opacity(90, on: .hover)
             .flex(direction: .row, justify: .center)
         let rendered = element.render()
         #expect(
             rendered.contains(
-                "class=\"bg-blue-600 border-1 border-solid border-blue-800 shadow-md hover:opacity-90 flex flex-row justify-center\""
+                "class=\"bg-blue-600 border-1 border-solid border-blue-800 hover:opacity-90 flex flex-row justify-center\""
             )
         )
     }
diff --git a/Tests/WebUITests/Styles/BaseTests.swift b/Tests/WebUITests/Styles/BaseTests.swift
index 0ee6ddb7..8843b0b2 100644
--- a/Tests/WebUITests/Styles/BaseTests.swift
+++ b/Tests/WebUITests/Styles/BaseTests.swift
@@ -298,21 +298,21 @@ import Testing
 
     @Test("Spacing with horizontal direction")
     func testSpacingWithHorizontalDirection() async throws {
-        let element = Element(tag: "div").spacing(of: 6, along: .x)
+        let element = Element(tag: "div").spacing(of: 6, along: .horizontal)
         let rendered = element.render()
         #expect(rendered.contains("class=\"space-x-6\""))
     }
 
     @Test("Spacing with along parameter only")
     func testSpacingWithAlongParameterOnly() async throws {
-        let element = Element(tag: "div").spacing(along: .y)
+        let element = Element(tag: "div").spacing(along: .vertical)
         let rendered = element.render()
         #expect(rendered.contains("class=\"space-y-4\""))
     }
 
     @Test("Spacing with modifier")
     func testSpacingWithModifier() async throws {
-        let element = Element(tag: "div").spacing(of: 2, along: .y, on: .lg)
+        let element = Element(tag: "div").spacing(of: 2, along: .vertical, on: .lg)
         let rendered = element.render()
         #expect(rendered.contains("class=\"lg:space-y-2\""))
     }
@@ -433,7 +433,7 @@ import Testing
 
     @Test("Spacing with nil length")
     func testSpacingWithNilLength() async throws {
-        let element = Element(tag: "div").spacing(of: nil, along: .x)
+        let element = Element(tag: "div").spacing(of: nil, along: .vertical)
         let rendered = element.render()
         #expect(!rendered.contains("class="))
     }
diff --git a/Tests/WebUITests/Styles/BorderStyleOperationTests.swift b/Tests/WebUITests/Styles/BorderStyleOperationTests.swift
new file mode 100644
index 00000000..e46e748b
--- /dev/null
+++ b/Tests/WebUITests/Styles/BorderStyleOperationTests.swift
@@ -0,0 +1,218 @@
+import Testing
+
+@testable import WebUI
+
+@Suite("Border Style Operation Tests") struct BorderStyleOperationTests {
+    // MARK: - Basic Style Operations Tests
+
+    @Test("Border with default parameters")
+    func testBorderWithDefaultParameters() async throws {
+        let params = BorderStyleOperation.Parameters()
+        let classes = BorderStyleOperation.shared.applyClasses(params: params)
+
+        #expect(classes.contains("border-1") || classes.contains("border"))
+    }
+
+    @Test("Border with specific width")
+    func testBorderWithSpecificWidth() async throws {
+        let params = BorderStyleOperation.Parameters(width: 2)
+        let classes = BorderStyleOperation.shared.applyClasses(params: params)
+
+        #expect(classes.contains("border-2"))
+    }
+
+    @Test("Border with specific edge")
+    func testBorderWithSpecificEdge() async throws {
+        let params = BorderStyleOperation.Parameters(edges: [.top])
+        let classes = BorderStyleOperation.shared.applyClasses(params: params)
+
+        #expect(classes.contains("border-t-1") || classes.contains("border-t"))
+    }
+
+    @Test("Border with style")
+    func testBorderWithStyle() async throws {
+        let params = BorderStyleOperation.Parameters(style: .dashed)
+        let classes = BorderStyleOperation.shared.applyClasses(params: params)
+
+        #expect(classes.contains("border-dashed"))
+    }
+
+    @Test("Border with color")
+    func testBorderWithColor() async throws {
+        let params = BorderStyleOperation.Parameters(color: .blue(._500))
+        let classes = BorderStyleOperation.shared.applyClasses(params: params)
+
+        #expect(classes.contains("border-blue-500"))
+    }
+
+    @Test("Border with divide style")
+    func testBorderWithDivideStyle() async throws {
+        let params = BorderStyleOperation.Parameters(
+            width: 2,
+            edges: [.horizontal],
+            style: .divide
+        )
+        let classes = BorderStyleOperation.shared.applyClasses(params: params)
+
+        #expect(classes.contains("divide-x-2"))
+    }
+
+    // MARK: - Element Tests
+
+    @Test("Element extension with default parameters")
+    func testElementExtensionWithDefaultParameters() async throws {
+        let element = Element(tag: "div").border()
+        let rendered = element.render()
+
+        #expect(rendered.contains("class=\"border-1\"") || rendered.contains("class=\"border\""))
+    }
+
+    @Test("Element extension with specific width")
+    func testElementExtensionWithSpecificWidth() async throws {
+        let element = Element(tag: "div").border(of: 3)
+        let rendered = element.render()
+
+        #expect(rendered.contains("class=\"border-3\""))
+    }
+
+    @Test("Element extension with specific edges")
+    func testElementExtensionWithSpecificEdges() async throws {
+        let element = Element(tag: "div").border(at: .top, .bottom)
+        let rendered = element.render()
+
+        #expect(rendered.contains("border-t") && rendered.contains("border-b"))
+    }
+
+    @Test("Element extension with color and style")
+    func testElementExtensionWithColorAndStyle() async throws {
+        let element = Element(tag: "div").border(style: .dotted, color: .red(._300))
+        let rendered = element.render()
+
+        #expect(rendered.contains("border-dotted") && rendered.contains("border-red-300"))
+    }
+
+    @Test("Element extension with modifiers")
+    func testElementExtensionWithModifiers() async throws {
+        let element = Element(tag: "div").border(of: 2, color: .blue(._500), on: .hover, .md)
+        let rendered = element.render()
+
+        #expect(rendered.contains("hover:md:border-2") && rendered.contains("hover:md:border-blue-500"))
+    }
+
+    // MARK: - Responsive Modification Tests
+
+    @Test("Responsive modification with result builder syntax")
+    func testResponsiveModificationWithResultBuilderSyntax() async throws {
+        let element = Element(tag: "div").responsive {
+            sm {
+                border(of: 1, color: .gray(._200))
+            }
+            md {
+                border(of: 2, style: .dashed, color: .blue(._500))
+            }
+        }
+
+        let rendered = element.render()
+
+        #expect(rendered.contains("sm:border-1") && rendered.contains("sm:border-gray-200"))
+        #expect(
+            rendered.contains("md:border-2") && rendered.contains("md:border-dashed")
+                && rendered.contains("md:border-blue-500")
+        )
+    }
+
+    @Test("Responsive modification with all breakpoints")
+    func testResponsiveModificationWithAllBreakpoints() async throws {
+        let element = Element(tag: "div").responsive {
+            xs {
+                border(of: 1)
+            }
+            sm {
+                border(at: .top)
+            }
+            md {
+                border(style: .dashed)
+            }
+            lg {
+                border(color: .gray(._300))
+            }
+            xl {
+                border(of: 2, at: .bottom)
+            }
+            xl2 {
+                border(of: 3, style: .dotted, color: .blue(._500))
+            }
+        }
+
+        let rendered = element.render()
+
+        #expect(rendered.contains("xs:border-1"))
+        #expect(rendered.contains("sm:border-t"))
+        #expect(rendered.contains("md:border-dashed"))
+        #expect(rendered.contains("lg:border-gray-300"))
+        #expect(rendered.contains("xl:border-b-2"))
+        #expect(
+            rendered.contains("2xl:border-3") && rendered.contains("2xl:border-dotted")
+                && rendered.contains("2xl:border-blue-500")
+        )
+    }
+
+    // MARK: - Complex Scenarios
+
+    @Test("Complex element with multiple border styles")
+    func testComplexElementWithMultipleBorderStyles() async throws {
+        let element = Element(tag: "div")
+            .border(of: 1, at: .horizontal)
+            .border(of: 2, at: .vertical, color: .gray(._300))
+            .responsive {
+                md {
+                    border(of: 0, at: .top)
+                    border(style: .dashed)
+                }
+                lg {
+                    border(of: 3, color: .blue(._500))
+                }
+            }
+
+        let rendered = element.render()
+
+        #expect(rendered.contains("border-x-1"))
+        #expect(rendered.contains("border-y-2") && rendered.contains("border-gray-300"))
+        #expect(rendered.contains("md:border-t-0") && rendered.contains("md:border-dashed"))
+        #expect(rendered.contains("lg:border-3") && rendered.contains("lg:border-blue-500"))
+    }
+
+    @Test("Element with divide style")
+    func testElementWithDivideStyle() async throws {
+        let element = Element(tag: "div")
+            .border(of: 2, at: .horizontal, style: .divide)
+            .responsive {
+                md {
+                    border(of: 4, at: .vertical, style: .divide)
+                }
+            }
+
+        let rendered = element.render()
+
+        #expect(rendered.contains("divide-x-2"))
+        #expect(rendered.contains("md:divide-y-4"))
+    }
+
+    // MARK: - Edge Cases
+
+    @Test("Border with nil width")
+    func testBorderWithNilWidth() async throws {
+        let params = BorderStyleOperation.Parameters(width: nil, color: .blue(._500))
+        let classes = BorderStyleOperation.shared.applyClasses(params: params)
+
+        #expect(classes.contains("border") || classes.contains("border-blue-500"))
+    }
+
+    @Test("Border with empty edges")
+    func testBorderWithEmptyEdges() async throws {
+        let params = BorderStyleOperation.Parameters(edges: [])
+        let classes = BorderStyleOperation.shared.applyClasses(params: params)
+
+        #expect(classes.contains("border") || classes.contains("border-1"))
+    }
+}
diff --git a/Tests/WebUITests/Styles/EdgeInsetsTests.swift b/Tests/WebUITests/Styles/EdgeInsetsTests.swift
deleted file mode 100644
index 61a1e366..00000000
--- a/Tests/WebUITests/Styles/EdgeInsetsTests.swift
+++ /dev/null
@@ -1,307 +0,0 @@
-import Testing
-
-@testable import WebUI
-
-@Suite("EdgeInsets Tests") struct EdgeInsetsTests {
-
-    // MARK: - EdgeInsets Initialization Tests
-
-    @Test("EdgeInsets with specific values for each edge")
-    func testEdgeInsetsWithSpecificValues() async throws {
-        let insets = EdgeInsets(top: 4, leading: 6, bottom: 8, trailing: 6)
-        #expect(insets.top == 4)
-        #expect(insets.leading == 6)
-        #expect(insets.bottom == 8)
-        #expect(insets.trailing == 6)
-    }
-
-    @Test("EdgeInsets with same value for all edges")
-    func testEdgeInsetsWithSameValue() async throws {
-        let insets = EdgeInsets(all: 5)
-        #expect(insets.top == 5)
-        #expect(insets.leading == 5)
-        #expect(insets.bottom == 5)
-        #expect(insets.trailing == 5)
-    }
-
-    @Test("EdgeInsets with vertical and horizontal values")
-    func testEdgeInsetsWithVerticalHorizontal() async throws {
-        let insets = EdgeInsets(vertical: 3, horizontal: 8)
-        #expect(insets.top == 3)
-        #expect(insets.leading == 8)
-        #expect(insets.bottom == 3)
-        #expect(insets.trailing == 8)
-    }
-
-    // MARK: - Padding Tests
-
-    @Test("Padding with EdgeInsets")
-    func testPaddingWithEdgeInsets() async throws {
-        let element = Element(tag: "div").padding(EdgeInsets(top: 4, leading: 6, bottom: 8, trailing: 6))
-        let rendered = element.render()
-        #expect(rendered.contains("pt-4"))
-        #expect(rendered.contains("pl-6"))
-        #expect(rendered.contains("pb-8"))
-        #expect(rendered.contains("pr-6"))
-    }
-
-    @Test("Padding with uniform EdgeInsets")
-    func testPaddingWithUniformEdgeInsets() async throws {
-        let element = Element(tag: "div").padding(EdgeInsets(all: 5))
-        let rendered = element.render()
-        #expect(rendered.contains("pt-5"))
-        #expect(rendered.contains("pl-5"))
-        #expect(rendered.contains("pb-5"))
-        #expect(rendered.contains("pr-5"))
-    }
-
-    @Test("Padding with EdgeInsets and modifiers")
-    func testPaddingWithEdgeInsetsAndModifiers() async throws {
-        let element = Element(tag: "div").padding(EdgeInsets(vertical: 3, horizontal: 8), on: .hover, .md)
-        let rendered = element.render()
-        #expect(rendered.contains("hover:md:pt-3"))
-        #expect(rendered.contains("hover:md:pl-8"))
-        #expect(rendered.contains("hover:md:pb-3"))
-        #expect(rendered.contains("hover:md:pr-8"))
-    }
-
-    // MARK: - Margins Tests
-
-    @Test("Margins with EdgeInsets")
-    func testMarginsWithEdgeInsets() async throws {
-        let element = Element(tag: "div").margins(EdgeInsets(top: 2, leading: 4, bottom: 6, trailing: 8))
-        let rendered = element.render()
-        #expect(rendered.contains("mt-2"))
-        #expect(rendered.contains("ml-4"))
-        #expect(rendered.contains("mb-6"))
-        #expect(rendered.contains("mr-8"))
-    }
-
-    @Test("Margins with uniform EdgeInsets")
-    func testMarginsWithUniformEdgeInsets() async throws {
-        let element = Element(tag: "div").margins(EdgeInsets(all: 4))
-        let rendered = element.render()
-        #expect(rendered.contains("mt-4"))
-        #expect(rendered.contains("ml-4"))
-        #expect(rendered.contains("mb-4"))
-        #expect(rendered.contains("mr-4"))
-    }
-
-    @Test("Auto margins with EdgeInsets")
-    func testAutoMarginsWithEdgeInsets() async throws {
-        let element = Element(tag: "div").margins(EdgeInsets(vertical: 0, horizontal: 0), auto: true)
-        let rendered = element.render()
-        #expect(rendered.contains("mt-auto"))
-        #expect(rendered.contains("ml-auto"))
-        #expect(rendered.contains("mb-auto"))
-        #expect(rendered.contains("mr-auto"))
-    }
-
-    @Test("Margins with EdgeInsets and modifiers")
-    func testMarginsWithEdgeInsetsAndModifiers() async throws {
-        let element = Element(tag: "div").margins(EdgeInsets(all: 3), on: .lg)
-        let rendered = element.render()
-        #expect(rendered.contains("lg:mt-3"))
-        #expect(rendered.contains("lg:ml-3"))
-        #expect(rendered.contains("lg:mb-3"))
-        #expect(rendered.contains("lg:mr-3"))
-    }
-
-    // MARK: - Border Tests
-
-    @Test("Border with EdgeInsets")
-    func testBorderWithEdgeInsets() async throws {
-        let element = Element(tag: "div").border(EdgeInsets(top: 1, leading: 2, bottom: 3, trailing: 4))
-        let rendered = element.render()
-        #expect(rendered.contains("border-t-1"))
-        #expect(rendered.contains("border-l-2"))
-        #expect(rendered.contains("border-b-3"))
-        #expect(rendered.contains("border-r-4"))
-    }
-
-    @Test("Border with EdgeInsets and style")
-    func testBorderWithEdgeInsetsAndStyle() async throws {
-        let element = Element(tag: "div").border(EdgeInsets(all: 2), style: .dashed)
-        let rendered = element.render()
-        #expect(rendered.contains("border-t-2"))
-        #expect(rendered.contains("border-l-2"))
-        #expect(rendered.contains("border-b-2"))
-        #expect(rendered.contains("border-r-2"))
-        #expect(rendered.contains("border-dashed"))
-    }
-
-    @Test("Border with EdgeInsets, style, and color")
-    func testBorderWithEdgeInsetsStyleAndColor() async throws {
-        let element = Element(tag: "div").border(
-            EdgeInsets(vertical: 1, horizontal: 2),
-            style: .solid,
-            color: .blue(._500)
-        )
-        let rendered = element.render()
-        #expect(rendered.contains("border-t-1"))
-        #expect(rendered.contains("border-l-2"))
-        #expect(rendered.contains("border-b-1"))
-        #expect(rendered.contains("border-r-2"))
-        #expect(rendered.contains("border-solid"))
-        #expect(rendered.contains("border-blue-500"))
-    }
-
-    @Test("Border with EdgeInsets and modifiers")
-    func testBorderWithEdgeInsetsAndModifiers() async throws {
-        let element = Element(tag: "div").border(EdgeInsets(all: 1), on: .hover)
-        let rendered = element.render()
-        #expect(rendered.contains("hover:border-t-1"))
-        #expect(rendered.contains("hover:border-l-1"))
-        #expect(rendered.contains("hover:border-b-1"))
-        #expect(rendered.contains("hover:border-r-1"))
-    }
-
-    // MARK: - Position Tests
-
-    @Test("Position with EdgeInsets")
-    func testPositionWithEdgeInsets() async throws {
-        let element = Element(tag: "div").position(insets: EdgeInsets(top: 0, leading: 4, bottom: 8, trailing: 12))
-        let rendered = element.render()
-        #expect(rendered.contains("top-0"))
-        #expect(rendered.contains("left-4"))
-        #expect(rendered.contains("bottom-8"))
-        #expect(rendered.contains("right-12"))
-    }
-
-    @Test("Position with type and EdgeInsets")
-    func testPositionWithTypeAndEdgeInsets() async throws {
-        let element = Element(tag: "div").position(.absolute, insets: EdgeInsets(all: 0))
-        let rendered = element.render()
-        #expect(rendered.contains("absolute"))
-        #expect(rendered.contains("top-0"))
-        #expect(rendered.contains("left-0"))
-        #expect(rendered.contains("bottom-0"))
-        #expect(rendered.contains("right-0"))
-    }
-
-    @Test("Position with EdgeInsets and modifiers")
-    func testPositionWithEdgeInsetsAndModifiers() async throws {
-        let element = Element(tag: "div").position(.sticky, insets: EdgeInsets(vertical: 0, horizontal: 4), on: .lg)
-        let rendered = element.render()
-        #expect(rendered.contains("lg:sticky"))
-        #expect(rendered.contains("lg:top-0"))
-        #expect(rendered.contains("lg:left-4"))
-        #expect(rendered.contains("lg:bottom-0"))
-        #expect(rendered.contains("lg:right-4"))
-    }
-
-    // MARK: - Responsive Tests
-
-    @Test("Element with responsive padding EdgeInsets")
-    func testElementWithResponsivePaddingEdgeInsets() async throws {
-        let element = Element(tag: "div").responsive {
-            $0.md {
-                $0.padding(EdgeInsets(top: 2, leading: 4, bottom: 6, trailing: 8))
-            }
-        }
-        let rendered = element.render()
-        #expect(rendered.contains("md:pt-2"))
-        #expect(rendered.contains("md:pl-4"))
-        #expect(rendered.contains("md:pb-6"))
-        #expect(rendered.contains("md:pr-8"))
-    }
-
-    @Test("Element with responsive margins EdgeInsets")
-    func testElementWithResponsiveMarginsEdgeInsets() async throws {
-        let element = Element(tag: "div").responsive {
-            $0.sm {
-                $0.margins(EdgeInsets(all: 5))
-            }
-        }
-        let rendered = element.render()
-        #expect(rendered.contains("sm:mt-5"))
-        #expect(rendered.contains("sm:ml-5"))
-        #expect(rendered.contains("sm:mb-5"))
-        #expect(rendered.contains("sm:mr-5"))
-    }
-
-    @Test("Element with responsive auto margins EdgeInsets")
-    func testElementWithResponsiveAutoMarginsEdgeInsets() async throws {
-        let element = Element(tag: "div").responsive {
-            $0.lg {
-                $0.margins(EdgeInsets(vertical: 0, horizontal: 0), auto: true)
-            }
-        }
-        let rendered = element.render()
-        #expect(rendered.contains("lg:mt-auto"))
-        #expect(rendered.contains("lg:ml-auto"))
-        #expect(rendered.contains("lg:mb-auto"))
-        #expect(rendered.contains("lg:mr-auto"))
-    }
-
-    @Test("Element with responsive border EdgeInsets")
-    func testElementWithResponsiveBorderEdgeInsets() async throws {
-        let element = Element(tag: "div").responsive {
-            $0.xl {
-                $0.border(EdgeInsets(top: 1, leading: 2, bottom: 3, trailing: 4), style: .solid, color: .gray(._300))
-            }
-        }
-        let rendered = element.render()
-        #expect(rendered.contains("xl:border-t-1"))
-        #expect(rendered.contains("xl:border-l-2"))
-        #expect(rendered.contains("xl:border-b-3"))
-        #expect(rendered.contains("xl:border-r-4"))
-        #expect(rendered.contains("xl:border-solid"))
-        #expect(rendered.contains("xl:border-gray-300"))
-    }
-
-    @Test("Element with responsive position EdgeInsets")
-    func testElementWithResponsivePositionEdgeInsets() async throws {
-        let element = Element(tag: "div").responsive {
-            $0.lg {
-                $0.position(.fixed, insets: EdgeInsets(all: 0))
-            }
-        }
-        let rendered = element.render()
-        #expect(rendered.contains("lg:fixed"))
-        #expect(rendered.contains("lg:top-0"))
-        #expect(rendered.contains("lg:left-0"))
-        #expect(rendered.contains("lg:bottom-0"))
-        #expect(rendered.contains("lg:right-0"))
-    }
-
-    // MARK: - Complex Usage Tests
-
-    @Test("Complex element with multiple EdgeInsets usages")
-    func testComplexElementWithMultipleEdgeInsetsUsages() async throws {
-        let element = Element(tag: "div")
-            .padding(EdgeInsets(vertical: 4, horizontal: 8))
-            .margins(EdgeInsets(top: 2, leading: 0, bottom: 2, trailing: 0))
-            .border(EdgeInsets(top: 1, leading: 0, bottom: 1, trailing: 0), style: .solid, color: .gray(._200))
-            .position(.relative, insets: EdgeInsets(top: 2, leading: 0, bottom: 0, trailing: 0))
-
-        let rendered = element.render()
-
-        // Padding assertions
-        #expect(rendered.contains("pt-4"))
-        #expect(rendered.contains("pl-8"))
-        #expect(rendered.contains("pb-4"))
-        #expect(rendered.contains("pr-8"))
-
-        // Margins assertions
-        #expect(rendered.contains("mt-2"))
-        #expect(rendered.contains("ml-0"))
-        #expect(rendered.contains("mb-2"))
-        #expect(rendered.contains("mr-0"))
-
-        // Border assertions
-        #expect(rendered.contains("border-t-1"))
-        #expect(rendered.contains("border-l-0"))
-        #expect(rendered.contains("border-b-1"))
-        #expect(rendered.contains("border-r-0"))
-        #expect(rendered.contains("border-solid"))
-        #expect(rendered.contains("border-gray-200"))
-
-        // Position assertions
-        #expect(rendered.contains("relative"))
-        #expect(rendered.contains("top-2"))
-        #expect(rendered.contains("left-0"))
-        #expect(rendered.contains("bottom-0"))
-        #expect(rendered.contains("right-0"))
-    }
-}
diff --git a/Tests/WebUITests/Styles/InteractionModifiersTests.swift b/Tests/WebUITests/Styles/InteractionModifiersTests.swift
new file mode 100644
index 00000000..b383b16b
--- /dev/null
+++ b/Tests/WebUITests/Styles/InteractionModifiersTests.swift
@@ -0,0 +1,196 @@
+import Testing
+@testable import WebUI
+
+@Suite("Interaction Modifiers Tests")
+struct InteractionModifiersTests {
+    @Test("Hover state modifier")
+    func testHoverStateModifier() async throws {
+        let element = Element(tag: "div")
+            .on {
+                hover {
+                    background(color: .blue(._500))
+                    font(color: .gray(._50))
+                }
+            }
+        
+        let rendered = element.render()
+        #expect(rendered.contains("hover:bg-blue-500"))
+        #expect(rendered.contains("hover:text-gray-50"))
+    }
+    
+    @Test("Focus state modifier")
+    func testFocusStateModifier() async throws {
+        let element = Element(tag: "div")
+            .on {
+                focus {
+                    border(of: 2, color: .blue(._500))
+                    outline(of: 0)
+                }
+            }
+        
+        let rendered = element.render()
+        #expect(rendered.contains("focus:border-2"))
+        #expect(rendered.contains("focus:border-blue-500"))
+        #expect(rendered.contains("focus:outline-0"))
+    }
+    
+    @Test("Multiple state modifiers")
+    func testMultipleStateModifiers() async throws {
+        let element = Element(tag: "div")
+            .background(color: .gray(._100))
+            .padding(of: 4)
+            .on {
+                hover {
+                    background(color: .gray(._200))
+                }
+                focus {
+                    background(color: .blue(._100))
+                    border(of: 1, color: .blue(._500))
+                }
+                active {
+                    background(color: .blue(._200))
+                }
+            }
+        
+        let rendered = element.render()
+        #expect(rendered.contains("bg-gray-100"))
+        #expect(rendered.contains("p-4"))
+        #expect(rendered.contains("hover:bg-gray-200"))
+        #expect(rendered.contains("focus:bg-blue-100"))
+        #expect(rendered.contains("focus:border-1"))
+        #expect(rendered.contains("focus:border-blue-500"))
+        #expect(rendered.contains("active:bg-blue-200"))
+    }
+    
+    @Test("ARIA state modifiers")
+    func testAriaStateModifiers() async throws {
+        let element = Element(tag: "div")
+            .on {
+                ariaExpanded {
+                    border(of: 1, color: .gray(._300))
+                }
+                ariaSelected {
+                    background(color: .blue(._100))
+                    font(weight: .bold)
+                }
+            }
+        
+        let rendered = element.render()
+        #expect(rendered.contains("aria-expanded:border-1"))
+        #expect(rendered.contains("aria-expanded:border-gray-300"))
+        #expect(rendered.contains("aria-selected:bg-blue-100"))
+        #expect(rendered.contains("aria-selected:font-bold"))
+    }
+    
+    @Test("Placeholder modifier")
+    func testPlaceholderModifier() async throws {
+        let element = Element(tag: "input")
+            .on {
+                placeholder {
+                    font(color: .gray(._400))
+                    font(weight: .light)
+                }
+            }
+        
+        let rendered = element.render()
+        #expect(rendered.contains("placeholder:text-gray-400"))
+        #expect(rendered.contains("placeholder:font-light"))
+    }
+    
+    @Test("First and last child modifiers")
+    func testFirstLastChildModifiers() async throws {
+        let element = Element(tag: "ul")
+            .on {
+                first {
+                    border(of: 0, at: .top)
+                }
+                last {
+                    border(of: 0, at: .bottom)
+                }
+            }
+        
+        let rendered = element.render()
+        #expect(rendered.contains("first:border-t-0"))
+        #expect(rendered.contains("last:border-b-0"))
+    }
+    
+    @Test("Disabled state modifier")
+    func testDisabledStateModifier() async throws {
+        let element = Element(tag: "button")
+            .on {
+                disabled {
+                    opacity(50)
+                    cursor(.notAllowed)
+                }
+            }
+        
+        let rendered = element.render()
+        #expect(rendered.contains("disabled:opacity-50"))
+        #expect(rendered.contains("disabled:cursor-not-allowed"))
+    }
+    
+    @Test("Motion reduce modifier")
+    func testMotionReduceModifier() async throws {
+        let element = Element(tag: "div")
+            .transition(of: .transform, for: 300)
+            .on {
+                motionReduce {
+                    transition(of: .transform, for: 0)
+                }
+            }
+        
+        let rendered = element.render()
+        #expect(rendered.contains("transition-transform"))
+        #expect(rendered.contains("duration-300"))
+        #expect(rendered.contains("motion-reduce:duration-0"))
+    }
+    
+    @Test("Complex interactive button")
+    func testComplexInteractiveButton() async throws {
+        let button = Element(tag: "button")
+            .padding(of: 4)
+            .background(color: .blue(._500))
+            .font(color: .gray(._50))
+            .rounded(.md)
+            .transition(of: .all, for: 150)
+            .on {
+                hover {
+                    background(color: .blue(._600))
+                    transform(scale: (x: 105, y: 105))
+                }
+                focus {
+                    outline(of: 2, color: .blue(._300))
+                    outline(style: .solid)
+                }
+                active {
+                    background(color: .blue(._700))
+                    transform(scale: (x: 95, y: 95))
+                }
+                disabled {
+                    background(color: .gray(._400))
+                    opacity(75)
+                    cursor(.notAllowed)
+                }
+            }
+        
+        let rendered = button.render()
+        #expect(rendered.contains("p-4"))
+        #expect(rendered.contains("bg-blue-500"))
+        #expect(rendered.contains("text-gray-50"))
+        #expect(rendered.contains("rounded-md"))
+        #expect(rendered.contains("transition-all"))
+        #expect(rendered.contains("duration-150"))
+        #expect(rendered.contains("hover:bg-blue-600"))
+        #expect(rendered.contains("hover:scale-x-105"))
+        #expect(rendered.contains("hover:scale-y-105"))
+        #expect(rendered.contains("focus:outline-2"))
+        #expect(rendered.contains("focus:outline-blue-300"))
+        #expect(rendered.contains("focus:outline-solid"))
+        #expect(rendered.contains("active:bg-blue-700"))
+        #expect(rendered.contains("active:scale-x-95"))
+        #expect(rendered.contains("active:scale-y-95"))
+        #expect(rendered.contains("disabled:bg-gray-400"))
+        #expect(rendered.contains("disabled:opacity-75"))
+        #expect(rendered.contains("disabled:cursor-not-allowed"))
+    }
+}
\ No newline at end of file
diff --git a/Tests/WebUITests/Styles/NewResponsiveTests.swift b/Tests/WebUITests/Styles/NewResponsiveTests.swift
new file mode 100644
index 00000000..d39e80c5
--- /dev/null
+++ b/Tests/WebUITests/Styles/NewResponsiveTests.swift
@@ -0,0 +1,198 @@
+import Testing
+
+@testable import WebUI
+
+@Suite("New Responsive Style Tests") struct NewResponsiveTests {
+    // MARK: - Basic Responsive Tests
+
+    @Test("Basic responsive font styling with result builder syntax")
+    func testBasicResponsiveFontStylingNewSyntax() async throws {
+        let element = Element(tag: "div")
+            .font(size: .sm)
+            .responsive {
+                md {
+                    font(size: .lg)
+                }
+            }
+
+        let rendered = element.render()
+        #expect(rendered.contains("class=\"text-sm md:text-lg\""))
+    }
+
+    @Test("Multiple breakpoints with result builder syntax")
+    func testMultipleBreakpointsNewSyntax() async throws {
+        let element = Element(tag: "div")
+            .background(color: .gray(._100))
+            .font(size: .sm)
+            .on {
+                sm {
+                    font(size: .base)
+                }
+                md {
+                    font(size: .lg)
+                    background(color: .gray(._200))
+                }
+                lg {
+                    font(size: .xl)
+                    background(color: .gray(._300))
+                }
+            }
+
+        let rendered = element.render()
+        #expect(
+            rendered.contains(
+                "class=\"bg-gray-100 text-sm sm:text-base md:text-lg md:bg-gray-200 lg:text-xl lg:bg-gray-300\""
+            )
+        )
+    }
+
+    // MARK: - Multiple Style Types Tests
+
+    @Test("Multiple style types with result builder syntax")
+    func testMultipleStyleTypesNewSyntax() async throws {
+        let element = Element(tag: "div")
+            .padding(of: 2)
+            .font(size: .sm)
+            .on {
+                md {
+                    padding(of: 4)
+                    font(size: .lg)
+                    margins(of: 2)
+                }
+            }
+
+        let rendered = element.render()
+        #expect(rendered.contains("class=\"p-2 text-sm md:p-4 md:text-lg md:m-2\""))
+    }
+
+    // MARK: - Complex Component Tests
+
+    @Test("Complex component with result builder syntax")
+    func testComplexComponentNewSyntax() async throws {
+        let button = Button(type: .submit) { "Submit" }
+            .background(color: .blue(._500))
+            .font(color: .blue(._50))
+            .padding(of: 2)
+            .rounded(.md)
+            .on {
+                sm {
+                    padding(of: 3)
+                }
+                md {
+                    padding(of: 4)
+                    font(size: .lg)
+                }
+                lg {
+                    padding(of: 6)
+                    background(color: .blue(._600))
+                }
+            }
+
+        let rendered = button.render()
+        #expect(rendered.contains("type=\"submit\""))
+        #expect(rendered.contains("bg-blue-500"))
+        #expect(rendered.contains("text-blue-50"))
+        #expect(rendered.contains("p-2"))
+        #expect(rendered.contains("rounded-md"))
+        #expect(rendered.contains("sm:p-3"))
+        #expect(rendered.contains("md:p-4"))
+        #expect(rendered.contains("md:text-lg"))
+        #expect(rendered.contains("lg:p-6"))
+        #expect(rendered.contains("lg:bg-blue-600"))
+    }
+
+    // MARK: - Layout Tests
+
+    @Test("Responsive flex layout with result builder syntax")
+    func testResponsiveFlexLayoutNewSyntax() async throws {
+        let element = Element(tag: "div")
+            .flex(direction: .column)
+            .on {
+                md {
+                    flex(direction: .row, justify: .between)
+                }
+            }
+
+        let rendered = element.render()
+        #expect(rendered.contains("flex"))
+        #expect(rendered.contains("flex-col"))
+        #expect(rendered.contains("md:flex"))
+        #expect(rendered.contains("md:flex-row") || rendered.contains("md:row"))
+        #expect(rendered.contains("md:justify-between"))
+    }
+
+    @Test("Responsive grid layout with result builder syntax")
+    func testResponsiveGridLayoutNewSyntax() async throws {
+        let element = Element(tag: "div")
+            .grid(columns: 1)
+            .on {
+                md {
+                    grid(columns: 2)
+                }
+                lg {
+                    grid(columns: 3)
+                }
+            }
+
+        let rendered = element.render()
+        #expect(rendered.contains("grid"))
+        #expect(rendered.contains("grid-cols-1"))
+        #expect(rendered.contains("md:grid-cols-2"))
+        #expect(rendered.contains("lg:grid-cols-3"))
+    }
+
+    // MARK: - Visibility Tests
+
+    @Test("Responsive visibility with result builder syntax")
+    func testResponsiveVisibilityNewSyntax() async throws {
+        let element = Element(tag: "div")
+            .hidden()
+            .on {
+                md {
+                    hidden(false)
+                }
+            }
+
+        let rendered = element.render()
+        #expect(rendered.contains("class=\"hidden\""))
+        #expect(!rendered.contains("md:hidden"))
+    }
+
+    // MARK: - Backward Compatibility Test
+
+    @Test("Test all breakpoint modifiers")
+    func testAllBreakpointModifiers() async throws {
+        let element = Element(tag: "div")
+            .font(size: .sm)
+            .on {
+                xs {
+                    padding(of: 1)
+                }
+                sm {
+                    padding(of: 2)
+                }
+                md {
+                    font(size: .lg)
+                }
+                lg {
+                    margins(of: 3)
+                }
+                xl {
+                    border(of: 1, color: .blue(._500))
+                }
+                xl2 {
+                    background(color: .gray(._200))
+                }
+            }
+
+        let rendered = element.render()
+        #expect(rendered.contains("text-sm"))
+        #expect(rendered.contains("xs:p-1"))
+        #expect(rendered.contains("sm:p-2"))
+        #expect(rendered.contains("md:text-lg"))
+        #expect(rendered.contains("lg:m-3"))
+        #expect(rendered.contains("xl:border-1"))
+        #expect(rendered.contains("xl:border-blue-500"))
+        #expect(rendered.contains("2xl:bg-gray-200"))
+    }
+}
diff --git a/Tests/WebUITests/Styles/PositioningTests.swift b/Tests/WebUITests/Styles/PositioningTests.swift
index 8a31f567..21f92147 100644
--- a/Tests/WebUITests/Styles/PositioningTests.swift
+++ b/Tests/WebUITests/Styles/PositioningTests.swift
@@ -112,14 +112,14 @@ import Testing
 
     @Test("Overflow with specific axis")
     func testOverflowWithSpecificAxis() async throws {
-        let element = Element(tag: "div").overflow(.scroll, axis: .x)
+        let element = Element(tag: "div").overflow(.scroll, axis: .horizontal)
         let rendered = element.render()
         #expect(rendered.contains("class=\"overflow-x-scroll\""))
     }
 
     @Test("Overflow with modifier")
     func testOverflowWithModifier() async throws {
-        let element = Element(tag: "div").overflow(.auto, axis: .y, on: .lg)
+        let element = Element(tag: "div").overflow(.auto, axis: .vertical, on: .lg)
         let rendered = element.render()
         #expect(rendered.contains("class=\"lg:overflow-y-auto\""))
     }
@@ -144,7 +144,7 @@ import Testing
     func testTransformWithNegativeTranslate() async throws {
         let element = Element(tag: "div").transform(translate: (x: -10, y: 20))
         let rendered = element.render()
-        #expect(rendered.contains("class=\"transform translate-x-10 translate-y-20\""))
+        #expect(rendered.contains("class=\"transform -translate-x-10 translate-y-20\""))
     }
 
     @Test("Transform with skew and modifier")
@@ -163,7 +163,7 @@ import Testing
             skew: (x: nil, y: 10)
         )
         let rendered = element.render()
-        #expect(rendered.contains("class=\"transform scale-x-90 rotate-30 translate-x-5 skew-y-10\""))
+        #expect(rendered.contains("class=\"transform scale-x-90 -rotate-30 translate-x-5 skew-y-10\""))
     }
 
     // MARK: - Scroll Tests
@@ -237,7 +237,7 @@ import Testing
             .transition(of: .transform, for: 200, easing: .out)
             .zIndex(30)
             .position(.absolute, at: .top, .trailing, offset: 4)
-            .overflow(.hidden, axis: .y)
+            .overflow(.hidden, axis: .vertical)
             .transform(scale: (x: 95, y: 95), rotate: 15)
         let rendered = element.render()
         #expect(
diff --git a/Tests/WebUITests/Styles/ResponsiveDSLTests.swift b/Tests/WebUITests/Styles/ResponsiveDSLTests.swift
new file mode 100644
index 00000000..37208c45
--- /dev/null
+++ b/Tests/WebUITests/Styles/ResponsiveDSLTests.swift
@@ -0,0 +1,147 @@
+import Testing
+
+@testable import WebUI
+
+@Suite("Responsive DSL Tests") struct ResponsiveDSLTests {
+    // MARK: - Basic Tests
+
+    @Test("Basic font styling with result builder syntax")
+    func testBasicFontStylingWithDSL() async throws {
+        let element = Element(tag: "div")
+            .font(size: .sm)
+            .on {
+                md {
+                    font(size: .lg)
+                }
+            }
+
+        let rendered = element.render()
+        #expect(rendered.contains("text-sm"))
+        #expect(rendered.contains("md:text-lg"))
+    }
+
+    @Test("Multiple breakpoints with result builder syntax")
+    func testMultipleBreakpointsWithDSL() async throws {
+        let element = Element(tag: "div")
+            .background(color: .gray(._100))
+            .font(size: .sm)
+            .on {
+                sm {
+                    font(size: .base)
+                }
+                md {
+                    font(size: .lg)
+                    background(color: .gray(._200))
+                }
+                lg {
+                    font(size: .xl)
+                    background(color: .gray(._300))
+                }
+            }
+
+        let rendered = element.render()
+        #expect(rendered.contains("bg-gray-100"))
+        #expect(rendered.contains("text-sm"))
+        #expect(rendered.contains("sm:text-base"))
+        #expect(rendered.contains("md:text-lg"))
+        #expect(rendered.contains("md:bg-gray-200"))
+        #expect(rendered.contains("lg:text-xl"))
+        #expect(rendered.contains("lg:bg-gray-300"))
+    }
+
+    @Test("Multiple style types with result builder syntax")
+    func testMultipleStyleTypesWithDSL() async throws {
+        let element = Element(tag: "div")
+            .padding(of: 2)
+            .font(size: .sm)
+            .on {
+                md {
+                    padding(of: 4)
+                    font(size: .lg)
+                    margins(of: 2)
+                }
+            }
+
+        let rendered = element.render()
+        #expect(rendered.contains("p-2"))
+        #expect(rendered.contains("text-sm"))
+        #expect(rendered.contains("md:p-4"))
+        #expect(rendered.contains("md:text-lg"))
+        #expect(rendered.contains("md:m-2"))
+    }
+
+    // MARK: - Layout Tests
+
+    @Test("Flex layout with result builder syntax")
+    func testFlexLayoutWithDSL() async throws {
+        let element = Element(tag: "div")
+            .flex(direction: .column)
+            .on {
+                md {
+                    flex(direction: .row, justify: .between)
+                }
+            }
+
+        let rendered = element.render()
+        #expect(rendered.contains("flex"))
+        #expect(rendered.contains("flex-col"))
+        #expect(rendered.contains("md:flex"))
+        #expect(rendered.contains("md:justify-between"))
+    }
+
+    @Test("Grid layout with result builder syntax")
+    func testGridLayoutWithDSL() async throws {
+        let element = Element(tag: "div")
+            .grid(columns: 1)
+            .on {
+                md {
+                    grid(columns: 2)
+                }
+                lg {
+                    grid(columns: 3)
+                }
+            }
+
+        let rendered = element.render()
+        #expect(rendered.contains("grid"))
+        #expect(rendered.contains("grid-cols-1"))
+        #expect(rendered.contains("md:grid-cols-2"))
+        #expect(rendered.contains("lg:grid-cols-3"))
+    }
+
+    // MARK: - Backward Compatibility
+
+    @Test("Test all breakpoint modifiers")
+    func testAllBreakpointModifiers() async throws {
+        let element = Element(tag: "div")
+            .on {
+                xs {
+                    padding(of: 1)
+                }
+                sm {
+                    font(size: .sm)
+                }
+                md {
+                    background(color: .blue(._300))
+                }
+                lg {
+                    margins(of: 4)
+                }
+                xl {
+                    border(of: 1, color: .gray(._200))
+                }
+                xl2 {
+                    rounded(.lg)
+                }
+            }
+
+        let rendered = element.render()
+        #expect(rendered.contains("xs:p-1"))
+        #expect(rendered.contains("sm:text-sm"))
+        #expect(rendered.contains("md:bg-blue-300"))
+        #expect(rendered.contains("lg:m-4"))
+        #expect(rendered.contains("xl:border-1"))
+        #expect(rendered.contains("xl:border-gray-200"))
+        #expect(rendered.contains("2xl:rounded-lg"))
+    }
+}
diff --git a/Tests/WebUITests/Styles/ResponsiveStyleBuilderTests.swift b/Tests/WebUITests/Styles/ResponsiveStyleBuilderTests.swift
new file mode 100644
index 00000000..5152b64f
--- /dev/null
+++ b/Tests/WebUITests/Styles/ResponsiveStyleBuilderTests.swift
@@ -0,0 +1,219 @@
+import Testing
+
+@testable import WebUI
+
+@Suite("Responsive Style Builder Tests") struct ResponsiveStyleBuilderTests {
+    // MARK: - Basic Tests
+
+    @Test("Basic responsive font styling with result builder syntax")
+    func testBasicResponsiveFontStylingNewSyntax() async throws {
+        let element = Element(tag: "div")
+            .font(size: .sm)
+            .on {
+                md {
+                    font(size: .lg)
+                }
+            }
+
+        let rendered = element.render()
+        #expect(rendered.contains("class=\"text-sm md:text-lg\""))
+    }
+
+    @Test("Multiple breakpoints with result builder syntax")
+    func testMultipleBreakpointsNewSyntax() async throws {
+        let element = Element(tag: "div")
+            .background(color: .gray(._100))
+            .font(size: .sm)
+            .on {
+                sm {
+                    font(size: .base)
+                }
+                md {
+                    font(size: .lg)
+                    background(color: .gray(._200))
+                }
+                lg {
+                    font(size: .xl)
+                    background(color: .gray(._300))
+                }
+            }
+
+        let rendered = element.render()
+        #expect(
+            rendered.contains(
+                "class=\"bg-gray-100 text-sm sm:text-base md:text-lg md:bg-gray-200 lg:text-xl lg:bg-gray-300\""
+            )
+        )
+    }
+
+    // MARK: - Multiple Style Types Tests
+
+    @Test("Multiple style types with result builder syntax")
+    func testMultipleStyleTypesNewSyntax() async throws {
+        let element = Element(tag: "div")
+            .padding(of: 2)
+            .font(size: .sm)
+            .on {
+                md {
+                    padding(of: 4)
+                    font(size: .lg)
+                    margins(of: 2)
+                }
+            }
+
+        let rendered = element.render()
+        #expect(rendered.contains("class=\"p-2 text-sm md:p-4 md:text-lg md:m-2\""))
+    }
+
+    // MARK: - Complex Component Tests
+
+    @Test("Complex component with result builder syntax")
+    func testComplexComponentNewSyntax() async throws {
+        let button = Button(type: .submit) { "Submit" }
+            .background(color: .blue(._500))
+            .font(color: .blue(._50))
+            .padding(of: 2)
+            .rounded(.md)
+            .on {
+                sm {
+                    padding(of: 3)
+                }
+                md {
+                    padding(of: 4)
+                    font(size: .lg)
+                }
+                lg {
+                    padding(of: 6)
+                    background(color: .blue(._600))
+                }
+            }
+
+        let rendered = button.render()
+        #expect(rendered.contains("type=\"submit\""))
+        #expect(rendered.contains("bg-blue-500"))
+        #expect(rendered.contains("text-blue-50"))
+        #expect(rendered.contains("p-2"))
+        #expect(rendered.contains("rounded-md"))
+        #expect(rendered.contains("sm:p-3"))
+        #expect(rendered.contains("md:p-4"))
+        #expect(rendered.contains("md:text-lg"))
+        #expect(rendered.contains("lg:p-6"))
+        #expect(rendered.contains("lg:bg-blue-600"))
+    }
+
+    // MARK: - Layout Tests
+
+    @Test("Responsive flex layout with result builder syntax")
+    func testResponsiveFlexLayoutNewSyntax() async throws {
+        let element = Element(tag: "div")
+            .flex(direction: .column)
+            .on {
+                md {
+                    flex(direction: .row, justify: .between)
+                }
+            }
+
+        let rendered = element.render()
+        #expect(rendered.contains("flex"))
+        #expect(rendered.contains("flex-col"))
+        #expect(rendered.contains("md:flex"))
+        #expect(rendered.contains("md:flex-row"))
+        #expect(rendered.contains("md:justify-between"))
+    }
+
+    @Test("Responsive grid layout with result builder syntax")
+    func testResponsiveGridLayoutNewSyntax() async throws {
+        let element = Element(tag: "div")
+            .grid(columns: 1)
+            .on {
+                md {
+                    grid(columns: 2)
+                }
+                lg {
+                    grid(columns: 3)
+                }
+            }
+
+        let rendered = element.render()
+        #expect(rendered.contains("class=\"grid grid-cols-1 md:grid md:grid-cols-2 lg:grid lg:grid-cols-3\""))
+    }
+
+    // MARK: - Visibility Tests
+
+    @Test("Responsive visibility with result builder syntax")
+    func testResponsiveVisibilityNewSyntax() async throws {
+        let element = Element(tag: "div")
+            .hidden()
+            .responsive {
+                md {
+                    hidden(false)
+                }
+            }
+
+        let rendered = element.render()
+        #expect(rendered.contains("class=\"hidden\""))
+        #expect(!rendered.contains("md:hidden"))
+    }
+
+    // MARK: - Compound Tests
+
+    @Test("Responsive nav menu with result builder syntax")
+    func testResponsiveNavMenuNewSyntax() async throws {
+        let nav = Navigation {
+            Stack(classes: ["mobile-menu"])
+                .responsive {
+                    md {
+                        hidden()
+                    }
+                }
+
+            List(classes: ["desktop-menu"])
+                .hidden()
+                .responsive {
+                    md {
+                        hidden(false)
+                    }
+                }
+        }
+
+        let rendered = nav.render()
+        #expect(rendered.contains("class=\"mobile-menu md:hidden\""))
+        #expect(rendered.contains("class=\"desktop-menu hidden\""))
+    }
+
+    // MARK: - Backward Compatibility Test
+
+    @Test("Test all breakpoint modifiers")
+    func testAllBreakpointModifiers() async throws {
+        let element = Element(tag: "div")
+            .responsive {
+                xs {
+                    padding(of: 1)
+                }
+                sm {
+                    padding(of: 2)
+                }
+                md {
+                    font(size: .lg)
+                }
+                lg {
+                    margins(of: 3)
+                }
+                xl {
+                    border(of: 1, color: .blue(._500))
+                }
+                xl2 {
+                    background(color: .gray(._200))
+                }
+            }
+
+        let rendered = element.render()
+        #expect(rendered.contains("xs:p-1"))
+        #expect(rendered.contains("sm:p-2"))
+        #expect(rendered.contains("md:text-lg"))
+        #expect(rendered.contains("lg:m-3"))
+        #expect(rendered.contains("xl:border-1"))
+        #expect(rendered.contains("xl:border-blue-500"))
+        #expect(rendered.contains("2xl:bg-gray-200"))
+    }
+}
diff --git a/Tests/WebUITests/Styles/ResponsiveTests.swift b/Tests/WebUITests/Styles/ResponsiveTests.swift
index 2423ea49..5fdecbaa 100644
--- a/Tests/WebUITests/Styles/ResponsiveTests.swift
+++ b/Tests/WebUITests/Styles/ResponsiveTests.swift
@@ -10,8 +10,8 @@ import Testing
         let element = Element(tag: "div")
             .font(size: .sm)
             .responsive {
-                $0.md {
-                    $0.font(size: .lg)
+                md {
+                    font(size: .lg)
                 }
             }
 
@@ -25,16 +25,16 @@ import Testing
             .background(color: .gray(._100))
             .font(size: .sm)
             .responsive {
-                $0.sm {
-                    $0.font(size: .base)
+                sm {
+                    font(size: .base)
                 }
-                $0.md {
-                    $0.font(size: .lg)
-                    $0.background(color: .gray(._200))
+                md {
+                    font(size: .lg)
+                    background(color: .gray(._200))
                 }
-                $0.lg {
-                    $0.font(size: .xl)
-                    $0.background(color: .gray(._300))
+                lg {
+                    font(size: .xl)
+                    background(color: .gray(._300))
                 }
             }
 
@@ -54,10 +54,10 @@ import Testing
             .padding(of: 2)
             .font(size: .sm)
             .responsive {
-                $0.md {
-                    $0.padding(of: 4)
-                    $0.font(size: .lg)
-                    $0.margins(of: 2)
+                md {
+                    padding(of: 4)
+                    font(size: .lg)
+                    margins(of: 2)
                 }
             }
 
@@ -75,16 +75,16 @@ import Testing
             .padding(of: 2)
             .rounded(.md)
             .responsive {
-                $0.sm {
-                    $0.padding(of: 3)
+                sm {
+                    padding(of: 3)
                 }
-                $0.md {
-                    $0.padding(of: 4)
-                    $0.font(size: .lg)
+                md {
+                    padding(of: 4)
+                    font(size: .lg)
                 }
-                $0.lg {
-                    $0.padding(of: 6)
-                    $0.background(color: .blue(._600))
+                lg {
+                    padding(of: 6)
+                    background(color: .blue(._600))
                 }
             }
 
@@ -108,8 +108,8 @@ import Testing
         let element = Element(tag: "div")
             .flex(direction: .column)
             .responsive {
-                $0.md {
-                    $0.flex(direction: .row, justify: .between)
+                md {
+                    flex(direction: .row, justify: .between)
                 }
             }
 
@@ -119,23 +119,6 @@ import Testing
         #expect(rendered.contains("md:flex"))
     }
 
-    @Test("Responsive grid layout")
-    func testResponsiveGridLayout() async throws {
-        let element = Element(tag: "div")
-            .grid(columns: 1)
-            .responsive {
-                $0.md {
-                    $0.grid(columns: 2)
-                }
-                $0.lg {
-                    $0.grid(columns: 3)
-                }
-            }
-
-        let rendered = element.render()
-        #expect(rendered.contains("class=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3\""))
-    }
-
     // MARK: - Sizing & Position Tests
 
     @Test("Responsive sizing")
@@ -143,9 +126,9 @@ import Testing
         let element = Element(tag: "div")
             .frame(width: 100)
             .responsive {
-                $0.md {
-                    $0.frame(maxWidth: 600)
-                    $0.margins(at: .horizontal, auto: true)
+                md {
+                    frame(maxWidth: .spacing(600))
+                    margins(at: .horizontal, auto: true)
                 }
             }
 
@@ -158,9 +141,9 @@ import Testing
         let element = Element(tag: "div")
             .position(.relative)
             .responsive {
-                $0.lg {
-                    $0.position(.fixed, at: .top, offset: 0)
-                    $0.frame(width: 100)
+                lg {
+                    position(.fixed, at: .top, offset: 0)
+                    frame(width: .spacing(100))
                 }
             }
 
@@ -175,8 +158,8 @@ import Testing
         let element = Element(tag: "div")
             .hidden()
             .responsive {
-                $0.md {
-                    $0.hidden(false)
+                md {
+                    hidden(false)
                 }
             }
 
@@ -194,16 +177,16 @@ import Testing
         let nav = Navigation {
             Stack(classes: ["mobile-menu"])
                 .responsive {
-                    $0.md {
-                        $0.hidden()
+                    md {
+                        hidden()
                     }
                 }
 
             List(classes: ["desktop-menu"])
                 .hidden()
                 .responsive {
-                    $0.md {
-                        $0.hidden(false)
+                    md {
+                        hidden(false)
                     }
                 }
         }
@@ -218,19 +201,65 @@ import Testing
         let hero = Section(classes: ["hero"])
             .padding(of: 4)
             .responsive {
-                $0.sm {
-                    $0.padding(of: 6)
+                sm {
+                    padding(of: 6)
                 }
-                $0.md {
-                    $0.padding(of: 8)
+                md {
+                    padding(of: 8)
                 }
-                $0.lg {
-                    $0.padding(of: 12)
-                    $0.frame(height: 100)
+                lg {
+                    padding(of: 12)
+                    frame(height: .spacing(100))
                 }
             }
 
         let rendered = hero.render()
         #expect(rendered.contains("class=\"hero p-4 sm:p-6 md:p-8 lg:p-12 lg:h-100\""))
     }
+
+    // MARK: - Testing All Breakpoint Modifiers
+
+    @Test("All breakpoint modifiers with different styles")
+    func testAllBreakpointModifiers() async throws {
+        let element = Element(tag: "div")
+            .responsive {
+                xs {
+                    padding(of: 1)
+                }
+                sm {
+                    padding(of: 2)
+                    font(size: .sm)
+                }
+                md {
+                    margins(of: 3)
+                    background(color: .blue(._100))
+                }
+                lg {
+                    font(size: .lg)
+                    border(of: 1, color: .gray(._300))
+                }
+                xl {
+                    padding(of: 4)
+                    position(.relative)
+                }
+                xl2 {
+                    margins(of: 5)
+                    rounded(.lg)
+                }
+            }
+
+        let rendered = element.render()
+        #expect(rendered.contains("xs:p-1"))
+        #expect(rendered.contains("sm:p-2"))
+        #expect(rendered.contains("sm:text-sm"))
+        #expect(rendered.contains("md:m-3"))
+        #expect(rendered.contains("md:bg-blue-100"))
+        #expect(rendered.contains("lg:text-lg"))
+        #expect(rendered.contains("lg:border-1"))
+        #expect(rendered.contains("lg:border-gray-300"))
+        #expect(rendered.contains("xl:p-4"))
+        #expect(rendered.contains("xl:relative"))
+        #expect(rendered.contains("2xl:m-5"))
+        #expect(rendered.contains("2xl:rounded-lg"))
+    }
 }
diff --git a/examples/InteractionModifiersExample.swift b/examples/InteractionModifiersExample.swift
new file mode 100644
index 00000000..ca73d35f
--- /dev/null
+++ b/examples/InteractionModifiersExample.swift
@@ -0,0 +1,280 @@
+import WebUI
+
+/// Interactive Button Component Example
+///
+/// This example demonstrates how to use interactive state modifiers to create
+/// a responsive and accessible button with various state-based styles.
+struct InteractiveButton: HTML {
+    var label: String
+    var isPrimary: Bool = true
+    var isDisabled: Bool = false
+    var onClick: String? = nil
+    
+    func render() -> String {
+        Button(disabled: isDisabled, onClick: onClick) { label }
+            // Base styles
+            .padding(of: 4)
+            .rounded(.md)
+            .transition(of: .all, for: 150)
+            .font(weight: .medium)
+            
+            // Conditional primary/secondary styles
+            .on {
+                if isPrimary {
+                    background(color: .blue(._600))
+                    font(color: .gray(._50))
+                } else {
+                    background(color: .gray(._200))
+                    font(color: .gray(._800))
+                    border(of: 1, color: .gray(._300))
+                }
+            }
+            
+            // Interactive state modifiers
+            .on {
+                // Hover state
+                hover {
+                    if isPrimary {
+                        background(color: .blue(._700))
+                    } else {
+                        background(color: .gray(._300))
+                    }
+                    transform(scale: (x: 102, y: 102))
+                }
+                
+                // Focus state (accessibility)
+                focus {
+                    if isPrimary {
+                        outline(of: 2, color: .blue(._300))
+                    } else {
+                        outline(of: 2, color: .gray(._400))
+                    }
+                    outline(style: .solid)
+                    transform(translateY: -1)
+                }
+                
+                // Active state (when pressing)
+                active {
+                    if isPrimary {
+                        background(color: .blue(._800))
+                    } else {
+                        background(color: .gray(._400))
+                    }
+                    transform(scale: (x: 98, y: 98))
+                }
+                
+                // Disabled state
+                disabled {
+                    if isPrimary {
+                        background(color: .blue(._300))
+                    } else {
+                        background(color: .gray(._100))
+                        font(color: .gray(._400))
+                    }
+                    opacity(70)
+                    cursor(.notAllowed)
+                }
+            }
+            .render()
+    }
+}
+
+/// Form Input Component Example
+///
+/// This example demonstrates how to create a form input with various
+/// interactive states including placeholder styling and ARIA states.
+struct FormInput: HTML {
+    var id: String
+    var label: String
+    var placeholder: String = ""
+    var isRequired: Bool = false
+    var isInvalid: Bool = false
+    var value: String? = nil
+    
+    func render() -> String {
+        Div {
+            Label(for: id) { label }
+                .font(size: .sm)
+                .font(weight: .medium)
+                .font(color: .gray(._700))
+                .on {
+                    if isRequired {
+                        after {
+                            font(color: .red(._500))
+                        }
+                    }
+                }
+            
+            Input(id: id, value: value, placeholder: placeholder, required: isRequired)
+                .padding(of: 3)
+                .rounded(.md)
+                .border(of: 1, color: .gray(._300))
+                .width(.full)
+                .font(size: .sm)
+                .transition(of: .all, for: 150)
+                
+                // Interactive state styling
+                .on {
+                    // Placeholder styling
+                    placeholder {
+                        font(color: .gray(._400))
+                        font(weight: .light)
+                    }
+                    
+                    // Focus state
+                    focus {
+                        border(of: 1, color: .blue(._500))
+                        shadow(of: .sm, color: .blue(._100))
+                    }
+                    
+                    // When the field is invalid
+                    if isInvalid {
+                        border(of: 1, color: .red(._500))
+                        
+                        // Invalid + focus state
+                        focus {
+                            border(of: 1, color: .red(._500))
+                            shadow(of: .sm, color: .red(._100))
+                        }
+                    }
+                    
+                    // ARIA required state
+                    ariaRequired {
+                        border(of: 1, style: .solid)
+                    }
+                }
+        }
+        .render()
+    }
+}
+
+/// Navigation Menu Item Example
+///
+/// This example demonstrates how to create an accessible navigation menu item
+/// with different states for hover, focus, active, and selected.
+struct NavMenuItem: HTML {
+    var label: String
+    var href: String
+    var isSelected: Bool = false
+    
+    func render() -> String {
+        Link(to: href) { label }
+            .padding(vertical: 2, horizontal: 4)
+            .rounded(.md)
+            .font(size: .sm)
+            .transition(of: .all, for: 150)
+            
+            // Base state
+            .on {
+                if isSelected {
+                    font(weight: .semibold)
+                    font(color: .blue(._700))
+                    background(color: .blue(._50))
+                } else {
+                    font(weight: .normal)
+                    font(color: .gray(._700))
+                }
+            }
+            
+            // Interactive states
+            .on {
+                // Hover state
+                hover {
+                    if !isSelected {
+                        background(color: .gray(._100))
+                    } else {
+                        background(color: .blue(._100))
+                    }
+                }
+                
+                // Focus state for keyboard navigation
+                focus {
+                    outline(of: 2, color: .blue(._300))
+                    outline(style: .solid)
+                    outline(offset: 1)
+                }
+                
+                // Active state (when pressing)
+                active {
+                    if !isSelected {
+                        background(color: .gray(._200))
+                    } else {
+                        background(color: .blue(._200))
+                    }
+                    transform(scale: (x: 98, y: 98))
+                }
+                
+                // ARIA selected state for screen readers
+                ariaSelected {
+                    font(weight: .semibold)
+                }
+            }
+            .render()
+    }
+}
+
+/// Example usage in a page context
+struct InteractiveComponentsDemo: HTML {
+    func render() -> String {
+        return Document(title: "Interactive Components Demo") {
+            Section {
+                Heading(level: 1) { "Interactive Components Demo" }
+                    .font(size: .xl2)
+                    .padding(bottom: 6)
+                
+                // Buttons section
+                Div {
+                    Heading(level: 2) { "Buttons" }
+                        .font(size: .xl)
+                        .padding(bottom: 4)
+                    
+                    Div {
+                        InteractiveButton(label: "Primary Button")
+                        InteractiveButton(label: "Secondary Button", isPrimary: false)
+                        InteractiveButton(label: "Disabled Button", isDisabled: true)
+                    }
+                    .spacing(of: 4)
+                    .display(.flex)
+                }
+                .padding(bottom: 8)
+                
+                // Form section
+                Div {
+                    Heading(level: 2) { "Form Inputs" }
+                        .font(size: .xl)
+                        .padding(bottom: 4)
+                    
+                    Div {
+                        FormInput(id: "name", label: "Name", placeholder: "Enter your name")
+                        FormInput(id: "email", label: "Email", placeholder: "Enter your email", isRequired: true)
+                        FormInput(id: "password", label: "Password", placeholder: "Enter your password", isInvalid: true)
+                    }
+                    .spacing(of: 4, along: .vertical)
+                }
+                .padding(bottom: 8)
+                
+                // Navigation section
+                Div {
+                    Heading(level: 2) { "Navigation" }
+                        .font(size: .xl)
+                        .padding(bottom: 4)
+                    
+                    Nav {
+                        Div {
+                            NavMenuItem(label: "Home", href: "/", isSelected: true)
+                            NavMenuItem(label: "Products", href: "/products")
+                            NavMenuItem(label: "About", href: "/about")
+                            NavMenuItem(label: "Contact", href: "/contact")
+                        }
+                        .display(.flex)
+                        .spacing(of: 2)
+                    }
+                }
+            }
+            .padding(of: 8)
+            .maxWidth(.character(80))
+            .margins(at: .horizontal, auto: true)
+        }
+        .render()
+    }
+}
\ No newline at end of file
diff --git a/examples/server/Sources/App/Views/Components/Layout.swift b/examples/server/Sources/App/Views/Components/Layout.swift
index dbd133e5..578cc477 100644
--- a/examples/server/Sources/App/Views/Components/Layout.swift
+++ b/examples/server/Sources/App/Views/Components/Layout.swift
@@ -22,7 +22,7 @@ struct Layout: HTML {
                     .transition(of: .colors)
                     .frame(width: 8, height: 8)
                     .flex(justify: .center, align: .center)
-                }.flex(align: .center).spacing(of: 2, along: .x)
+                }.flex(align: .center).spacing(of: 2, along: .vertical)
             }
             .flex(justify: .between, align: .center)
             .frame(width: .screen, maxWidth: .character(100))
@@ -40,7 +40,7 @@ struct Layout: HTML {
             .frame(maxWidth: .custom("99vw"))
             .frame(maxWidth: .character(76), on: .sm)
             .padding()
-            .spacing(of: 4, along: .y)
+            .spacing(of: 4, along: .horizontal)
 
             Footer {
                 Text {
diff --git a/examples/server/Sources/App/Views/Components/Styles.swift b/examples/server/Sources/App/Views/Components/Styles.swift
index 4f9e75ae..cd6241a3 100644
--- a/examples/server/Sources/App/Views/Components/Styles.swift
+++ b/examples/server/Sources/App/Views/Components/Styles.swift
@@ -13,7 +13,6 @@ extension Link {
 extension Element {
     func button(primary: Bool = false) -> Element {
         self
-            .padding(EdgeInsets(vertical: 2, horizontal: 4))
             .background(color: primary ? .blue(._500) : .gray(._700))
             .background(color: primary ? .blue(._700) : .gray(._900), on: .hover)
             .font(weight: .bold, color: .gray(._100))
diff --git a/examples/server/Sources/App/Views/Pages/Home.swift b/examples/server/Sources/App/Views/Pages/Home.swift
index beeedbbe..ae761966 100644
--- a/examples/server/Sources/App/Views/Pages/Home.swift
+++ b/examples/server/Sources/App/Views/Pages/Home.swift
@@ -29,11 +29,11 @@ struct HomeView: HTML {
                         "Read Documentation"
                     }
                     .button(primary: true)
-                }.spacing(of: 2, along: .x).margins(at: .top)
+                }.spacing(of: 2, along: .vertical).margins(at: .top)
             }
             .frame(maxWidth: .fraction(3, 4))
             .margins(at: .horizontal, auto: true)
-            .spacing(along: .y)
+            .spacing(along: .horizontal)
 
             // Information Section
             Stack {
@@ -53,13 +53,11 @@ struct HomeView: HTML {
             Form(action: "/", method: .post) {
                 Label(for: "name") { "Enter your name" }.font(weight: .bold)
                 Input(name: "name", type: .text, placeholder: "Your name...")
-                    .border(EdgeInsets(all: 1))
-                    .padding(EdgeInsets(vertical: 2, horizontal: 4))
                     .rounded(.lg)
                 Button(type: .submit) { "Enter" }.button(primary: true)
             }
             .flex(direction: .column)
-            .spacing(of: 2, along: .y)
+            .spacing(of: 2, along: .horizontal)
             .frame(maxWidth: .fraction(3, 4))
             .margins(at: .horizontal, auto: true)
             if let greeting {
diff --git a/examples/static/Sources/Components/Layout.swift b/examples/static/Sources/Components/Layout.swift
index 290cc005..f6d18cfc 100644
--- a/examples/static/Sources/Components/Layout.swift
+++ b/examples/static/Sources/Components/Layout.swift
@@ -22,7 +22,7 @@ struct Layout: HTML {
                             .font(size: .base, on: .md)
                             .font(decoration: .underline, on: .hover)
                     }
-                }.flex(align: .center).spacing(of: 2, along: .x)
+                }.flex(align: .center).spacing(of: 2, along: .vertical)
             }
             .flex(justify: .between, align: .center)
             .frame(width: .full, maxWidth: .character(86))
@@ -52,9 +52,9 @@ struct Layout: HTML {
         .frame(minHeight: .screen)
         .flex(direction: .column)
         .padding()
-        .responsive {
-            $0.md {
-                $0.font(size: .lg)
+        .on {
+            md {
+                font(size: .lg)
             }
         }
         .render()
diff --git a/examples/static/Sources/Pages/About.swift b/examples/static/Sources/Pages/About.swift
index 6465b7c3..07f1ec4b 100644
--- a/examples/static/Sources/Pages/About.swift
+++ b/examples/static/Sources/Pages/About.swift
@@ -51,9 +51,8 @@ struct About: HTML {
                 }
                 .background(color: .blue(._500))
                 .font(color: .custom("white"))
-                .padding(EdgeInsets(vertical: 2, horizontal: 4))
                 .rounded(.md)
-            }.spacing(of: 4, along: .y)
+            }.spacing(of: 4, along: .horizontal)
         }.render()
     }
 }
diff --git a/examples/static/Sources/Pages/Home.swift b/examples/static/Sources/Pages/Home.swift
index 1a253b1b..f5dca636 100644
--- a/examples/static/Sources/Pages/Home.swift
+++ b/examples/static/Sources/Pages/Home.swift
@@ -26,20 +26,18 @@ struct Home: HTML {
                     Link(to: "https://github.com/maclong9/web-ui", newTab: true) { "Source Code" }
                         .background(color: .neutral(._950))
                         .font(color: .neutral(._100))
-                        .padding(EdgeInsets(vertical: 2, horizontal: 2))
                         .rounded(.md)
                     Link(to: "https://maclong9.github.io/web-ui/documentation/webui/", newTab: true) {
                         "Read Documentation"
                     }
                     .background(color: .blue(._500))
                     .font(color: .neutral(._100))
-                    .padding(EdgeInsets(vertical: 2, horizontal: 2))
                     .rounded(.md)
                 }
-                .spacing(of: 2, along: .x)
+                .spacing(of: 2, along: .vertical)
                 .margins(at: .top)
             }
-            .spacing(of: 4, along: .y)
+            .spacing(of: 4, along: .horizontal)
             .margins(at: .horizontal, auto: true)
         }.render()
     }
diff --git a/initialize.swift b/initialize.swift
index f54662b3..2578dcb0 100755
--- a/initialize.swift
+++ b/initialize.swift
@@ -13,6 +13,7 @@ enum InitError: Error, CustomStringConvertible {
     case downloadFailed
     case templateNotFound(String)
     case fileReplacementFailed(String)
+    case contentReplacementFailed(Error)
 
     var description: String {
         switch self {
@@ -27,6 +28,8 @@ enum InitError: Error, CustomStringConvertible {
             return "Template not found at: \(path)"
         case .fileReplacementFailed(let path):
             return "Couldn't update content in '\(path)'"
+        case .contentReplacementFailed(let error):
+            return "Failed to replace content: \(error)"
         }
     }
 }
@@ -106,7 +109,7 @@ func replaceContentIn(file path: String, projectName: String) {
         content = content.replacingOccurrences(of: "example", with: projectName)
         try content.write(to: url, atomically: true, encoding: .utf8)
     } catch {
-        // Silently fail as in original
+        throw InitError.contentReplacementFailed(error)
     }
 }
 
@@ -114,9 +117,6 @@ func replaceContentIn(file path: String, projectName: String) {
 do {
     let (template, projectName) = try parseArgs()
 
-    // Both templates are now supported
-    // No need for early exit
-
     // Create temp directory
     let tmpDir = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)
     try FileManager.default.createDirectory(at: tmpDir, withIntermediateDirectories: true)