diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9d251b69..1844caa2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -40,10 +40,9 @@ This project and everyone participating in it is governed by the [WebUI Code of ### Pull Requests 1. Fork the repository -2. Create your feature branch from the `development` branch, (`git checkout -b feature/amazing-feature`) -3. Commit your changes following conventional commit messages (`git commit -m 'feat: add some amazing feature'`) -4. Push to the branch (`git push origin feature/amazing-feature`) -5. Open a Pull Request to the `development` branch. +2. Create your feature branch from the `development` branch, (`git checkout -b feature-00-amazing-feature`) +3. Push to the branch (`git push origin feature-00-amazing-feature`) +4. Open a Pull Request to the `development` branch. ## Development Process @@ -66,9 +65,9 @@ WebUI maintains three primary branches: Version bumps are triggered automatically via commit messages. Use the following prefixes: -- `feat!:` - Major version increment for breaking changes (e.g., `1.0.0` → `2.0.0`). -- `feat:` - Minor version increment for new features (e.g., `1.0.0` → `1.1.0`). -- `fix:` or `fix!:` - Patch version increment for bug fixes (e.g., `1.0.0` → `1.0.1`). +- `feat!:` or `Release` - Major version increment for breaking changes (e.g., `1.0.0` → `2.0.0`). +- `feat:` or `Feature` - Minor version increment for new features (e.g., `1.0.0` → `1.1.0`). +- `fix:`, `fix!:` or `Fixes` - Patch version increment for bug fixes (e.g., `1.0.0` → `1.0.1`). ### Release Process @@ -79,23 +78,11 @@ The release workflow is fully automated: 3. Release notes are automatically generated from the commit messages. 4. A new tag is created and the release is published on GitHub. -### Quick Fixes (Hotfixes) - -For urgent fixes that need to be pushed to `main` right away: - -1. Create a PR targeting the `main` branch. -2. Include `fix!:` in the PR title and merge message. -3. Once approved and merged, an action will automatically create PRs for `next` and `development` branches. -4. This ensures all branches remain in sync when quick changes are required in the main branch. - -> [!WARNING] -> Ensure the auto-generated PRs are approved and merged promptly to maintain branch synchronization. - ### Testing Automated tests run on every pull request to `main` and `next` branches: -1. Tests are executed in a macOS environment with Swift 6.1. +1. Tests are executed in a macOS environment. 2. The workflow includes caching of Swift Package Manager dependencies for faster builds. 3. All tests must pass before a PR can be merged. @@ -116,6 +103,9 @@ WebUI uses Swift DocC for documentation: swift package --disable-sandbox preview-documentation ``` +> [!NOTE] +> You can also run `Build Documentation` inside of Xcode to view the documentation in + ### Adding New Elements WebUI follows a compositional pattern for creating HTML elements. When adding a new element, adhere to these guidelines: @@ -139,12 +129,12 @@ WebUI follows a compositional pattern for creating HTML elements. When adding a /// Defines the types available for the element. /// /// Detailed documentation about the enum and its purpose. - public enum ElementType: String { + public enum ElementCustom: String { /// Documentation for this case. - case primary + case one /// Documentation for this case. - case secondary + case two } ``` @@ -155,12 +145,12 @@ WebUI follows a compositional pattern for creating HTML elements. When adding a /// Detailed documentation about what this element represents and its use cases. public final class ElementName: Element { // Properties specific to this element - let type: ElementType? + let customType: ElementCustom? /// Creates a new HTML element_name. /// /// - Parameters: - /// - type: Type of the element, optional. + /// - custom: An example custom attribute, optional. /// - id: Unique identifier, optional. /// - classes: CSS class names, optional. /// - role: ARIA role for accessibility, optional. @@ -187,7 +177,7 @@ WebUI follows a compositional pattern for creating HTML elements. When adding a // Build custom attributes using Attr namespace let customAttributes = [ - Attribute.typed("type", type) + Attribute.typed("custom", custom) // will generate as `custom="\(custom)"` ].compactMap { $0 } // Initialize the parent Element class @@ -205,7 +195,7 @@ WebUI follows a compositional pattern for creating HTML elements. When adding a } ``` -4. **Testing**: Add unit tests for the new element in the `Tests` directory. +4. **Testing**: Add unit tests for the new element in the `Tests/Styles` directory. 5. **Documentation**: Include comprehensive DocC documentation with: - Class-level documentation explaining the element's purpose @@ -213,6 +203,9 @@ WebUI follows a compositional pattern for creating HTML elements. When adding a - Usage examples showing common implementations - Mention any accessibility considerations +> [!IMPORTANT] +> Pull requests with new elements, modifiers and utilities will be rejected or put on hold until adequate documentation is provided. This is extemely important for both the end user of the library to understand what each element does and means semantically as well as ensuring maintainability for the maintainers of the project. + ## Adding New Style Modifiers Style modifiers in WebUI follow the unified style system pattern. Here's how to add a new style modifier: diff --git a/Sources/WebUI/Core/Metadata/Metadata.swift b/Sources/WebUI/Core/Infrastructure/Metadata/Metadata.swift similarity index 98% rename from Sources/WebUI/Core/Metadata/Metadata.swift rename to Sources/WebUI/Core/Infrastructure/Metadata/Metadata.swift index 20181411..be1e5994 100644 --- a/Sources/WebUI/Core/Metadata/Metadata.swift +++ b/Sources/WebUI/Core/Infrastructure/Metadata/Metadata.swift @@ -142,6 +142,8 @@ public struct Metadata { /// - locale: Override for the language locale. /// - type: Override for the content type. /// - themeColor: Override for the theme color. + /// - favicons: Override for the favicons. + /// - structuredData: Override for the structured data. /// /// - Example: /// ```swift diff --git a/Sources/WebUI/Core/Metadata/MetadataComponents.swift b/Sources/WebUI/Core/Infrastructure/Metadata/MetadataComponents.swift similarity index 100% rename from Sources/WebUI/Core/Metadata/MetadataComponents.swift rename to Sources/WebUI/Core/Infrastructure/Metadata/MetadataComponents.swift diff --git a/Sources/WebUI/Core/Metadata/MetadataTypes.swift b/Sources/WebUI/Core/Infrastructure/Metadata/MetadataTypes.swift similarity index 100% rename from Sources/WebUI/Core/Metadata/MetadataTypes.swift rename to Sources/WebUI/Core/Infrastructure/Metadata/MetadataTypes.swift diff --git a/Sources/WebUI/Core/Infrastructure/Robots/GenerateRobots.swift b/Sources/WebUI/Core/Infrastructure/Robots/GenerateRobots.swift new file mode 100644 index 00000000..5a79d41b --- /dev/null +++ b/Sources/WebUI/Core/Infrastructure/Robots/GenerateRobots.swift @@ -0,0 +1,80 @@ +import Foundation + +/// Provides functionality for generating robots.txt files. +/// +/// The `Robots` struct offers methods for creating standards-compliant robots.txt files +/// that provide instructions to web crawlers about which parts of a website they can access. +public struct Robots { + + /// Generates a robots.txt file content. + /// + /// This method creates a standard robots.txt file that includes instructions for web crawlers, + /// including a reference to the sitemap if one exists. + /// + /// - Parameters: + /// - baseURL: The optional base URL of the website (e.g., "https://example.com"). + /// - generateSitemap: Whether a sitemap is being generated for this website. + /// - robotsRules: Custom rules to include in the robots.txt file. + /// - Returns: A string containing the content of the robots.txt file. + /// + /// - Example: + /// ```swift + /// let content = Robots.generateTxt( + /// baseURL: "https://example.com", + /// generateSitemap: true, + /// robotsRules: [.allowAll()] + /// ) + /// ``` + /// + /// - Note: If custom rules are provided, they will be included in the file. + /// Otherwise, a default permissive robots.txt will be generated. + public static func generateTxt( + baseURL: String? = nil, + generateSitemap: Bool = false, + robotsRules: [RobotsRule]? = nil + ) -> String { + var contentComponents = ["# robots.txt generated by WebUI\n"] + + if let rules = robotsRules, !rules.isEmpty { + // Add each custom rule + for rule in rules { + contentComponents.append(formatRule(rule)) + } + } else { + // Default permissive robots.txt + contentComponents.append("User-agent: *\nAllow: /\n") + } + + // Add sitemap reference if applicable + if generateSitemap, let baseURL = baseURL { + contentComponents.append("Sitemap: \(baseURL)/sitemap.xml") + } + + return contentComponents.joined(separator: "\n") + } + + /// Formats a single robots rule as a string. + /// + /// - Parameter rule: The robots rule to format. + /// - Returns: A string representation of the rule. + private static func formatRule(_ rule: RobotsRule) -> String { + var ruleComponents = ["User-agent: \(rule.userAgent)"] + + // Add disallow paths + if let disallow = rule.disallow, !disallow.isEmpty { + ruleComponents.append(contentsOf: disallow.map { "Disallow: \($0)" }) + } + + // Add allow paths + if let allow = rule.allow, !allow.isEmpty { + ruleComponents.append(contentsOf: allow.map { "Allow: \($0)" }) + } + + // Add crawl delay if provided + if let crawlDelay = rule.crawlDelay { + ruleComponents.append("Crawl-delay: \(crawlDelay)") + } + + return ruleComponents.joined(separator: "\n") + } +} \ No newline at end of file diff --git a/Sources/WebUI/Core/Infrastructure/Robots/RobotsRule.swift b/Sources/WebUI/Core/Infrastructure/Robots/RobotsRule.swift new file mode 100644 index 00000000..36cb215a --- /dev/null +++ b/Sources/WebUI/Core/Infrastructure/Robots/RobotsRule.swift @@ -0,0 +1,104 @@ +import Foundation + +/// Represents a rule in a robots.txt file. +/// +/// Used to define instructions for web crawlers about which parts of the website should be crawled. +/// Each rule specifies which user agents (crawlers) it applies to and what paths they can access. +/// For more information about the robots.txt standard, see: https://developers.google.com/search/docs/crawling-indexing/robots/intro +public struct RobotsRule: Equatable, Hashable { + /// The user agent the rule applies to (e.g., "Googlebot" or "*" for all crawlers). + public let userAgent: String + + /// Paths that should not be crawled. + public let disallow: [String]? + + /// Paths that are allowed to be crawled (overrides disallow rules). + public let allow: [String]? + + /// The delay between successive crawls in seconds. + public let crawlDelay: Int? + + /// Creates a new robots.txt rule. + /// + /// - Parameters: + /// - userAgent: The user agent the rule applies to (e.g., "Googlebot" or "*" for all crawlers). + /// - disallow: Paths that should not be crawled. + /// - allow: Paths that are allowed to be crawled (overrides disallow rules). + /// - crawlDelay: The delay between successive crawls in seconds. + /// + /// - Example: + /// ```swift + /// let rule = RobotsRule( + /// userAgent: "*", + /// disallow: ["/admin/", "/private/"], + /// allow: ["/public/"], + /// crawlDelay: 10 + /// ) + /// ``` + public init( + userAgent: String, + disallow: [String]? = nil, + allow: [String]? = nil, + crawlDelay: Int? = nil + ) { + self.userAgent = userAgent + self.disallow = disallow + self.allow = allow + self.crawlDelay = crawlDelay + } + + /// Creates a rule that allows all crawlers to access the entire site. + /// + /// - Returns: A rule that allows all paths for all user agents. + /// + /// - Example: + /// ```swift + /// let allowAllRule = RobotsRule.allowAll() + /// ``` + public static func allowAll() -> RobotsRule { + RobotsRule(userAgent: "*", allow: ["/"]) + } + + /// Creates a rule that disallows all crawlers from accessing the entire site. + /// + /// - Returns: A rule that disallows all paths for all user agents. + /// + /// - Example: + /// ```swift + /// let disallowAllRule = RobotsRule.disallowAll() + /// ``` + public static func disallowAll() -> RobotsRule { + RobotsRule(userAgent: "*", disallow: ["/"]) + } + + /// Creates a rule for a specific crawler with custom access permissions. + /// + /// - Parameters: + /// - agent: The specific crawler user agent (e.g., "Googlebot"). + /// - disallow: Paths that should not be crawled. + /// - allow: Paths that are allowed to be crawled. + /// - crawlDelay: The delay between successive crawls in seconds. + /// - Returns: A rule configured for the specified crawler. + /// + /// - Example: + /// ```swift + /// let googleRule = RobotsRule.forAgent( + /// "Googlebot", + /// disallow: ["/private/"], + /// allow: ["/public/"] + /// ) + /// ``` + public static func forAgent( + _ agent: String, + disallow: [String]? = nil, + allow: [String]? = nil, + crawlDelay: Int? = nil + ) -> RobotsRule { + RobotsRule( + userAgent: agent, + disallow: disallow, + allow: allow, + crawlDelay: crawlDelay + ) + } +} \ No newline at end of file diff --git a/Sources/WebUI/Core/Infrastructure/Sitemap/GenerateSitemap.swift b/Sources/WebUI/Core/Infrastructure/Sitemap/GenerateSitemap.swift new file mode 100644 index 00000000..77b9273b --- /dev/null +++ b/Sources/WebUI/Core/Infrastructure/Sitemap/GenerateSitemap.swift @@ -0,0 +1,117 @@ +import Foundation + +/// Provides functionality for generating sitemap XML files. +/// +/// The `Sitemap` struct offers methods for creating standards-compliant sitemap.xml files +/// that inform search engines about the structure of a website and the relative importance +/// of its pages. Sitemaps improve SEO by ensuring all content is discoverable. +public struct Sitemap { + /// XML namespace for the sitemap protocol + private static let sitemapNamespace = "http://www.sitemaps.org/schemas/sitemap/0.9" + + /// Generates a sitemap.xml file content from website routes and custom entries. + /// + /// This method creates a standard sitemap.xml file that includes all routes in the website, + /// plus any additional custom sitemap entries. It follows the Sitemap protocol specification + /// from sitemaps.org. + /// + /// - Parameters: + /// - baseURL: The base URL of the website (e.g., "https://example.com"). + /// - routes: The document routes in the website. + /// - customEntries: Additional sitemap entries to include. + /// - Returns: A string containing the XML content of the sitemap. + /// + /// - Example: + /// ```swift + /// let sitemapXML = Sitemap.generateXML( + /// baseURL: "https://example.com", + /// routes: website.routes, + /// customEntries: additionalEntries + /// ) + /// ``` + /// + /// - Note: If any route has a `metadata.date` value, it will be used as the lastmod date + /// in the sitemap. Priority is set based on the path depth (home page gets 1.0). + public static func generateXML( + baseURL: String, + routes: [Document], + customEntries: [SitemapEntry]? = nil + ) -> String { + let dateFormatter = ISO8601DateFormatter() + + var xmlComponents = [ + "", + "" + ] + + // Add entries for all routes + for route in routes { + let path = route.path ?? "index" + let url = "\(baseURL)/\(path == "index" ? "" : "\(path).html")" + + var urlComponents = [" ", " \(url)"] + + // Add lastmod if metadata has a date + if let date = route.metadata.date { + urlComponents.append(" \(dateFormatter.string(from: date))") + } + + // Set priority based on path depth (home page gets higher priority) + let depth = path.components(separatedBy: "/").count + let priority = path == "index" ? 1.0 : max(0.5, 1.0 - Double(depth) * 0.1) + urlComponents.append(" \(String(format: "%.1f", priority))") + + // Add changefreq based on path (index and main sections change more frequently) + let changeFreq: SitemapEntry.ChangeFrequency = { + if path == "index" { + return .weekly + } else if depth == 1 { + return .monthly + } else { + return .yearly + } + }() + urlComponents.append(" \(changeFreq.rawValue)") + + urlComponents.append(" ") + xmlComponents.append(urlComponents.joined(separator: "\n")) + } + + // Add custom sitemap entries + if let customEntries = customEntries, !customEntries.isEmpty { + for entry in customEntries { + var urlComponents = [" ", " \(entry.url)"] + + if let lastMod = entry.lastModified { + urlComponents.append(" \(dateFormatter.string(from: lastMod))") + } + + if let changeFreq = entry.changeFrequency { + urlComponents.append(" \(changeFreq.rawValue)") + } + + if let priority = entry.priority { + urlComponents.append(" \(String(format: "%.1f", priority))") + } + + urlComponents.append(" ") + xmlComponents.append(urlComponents.joined(separator: "\n")) + } + } + + xmlComponents.append("") + return xmlComponents.joined(separator: "\n") + } + + /// Validates if a URL is properly formatted for inclusion in a sitemap. + /// + /// - Parameter url: The URL to validate. + /// - Returns: True if the URL is valid for a sitemap, false otherwise. + public static func isValidURL(_ url: String) -> Bool { + guard let url = URL(string: url) else { + return false + } + + return url.scheme != nil && url.host != nil + } +} \ No newline at end of file diff --git a/Sources/WebUI/Core/Infrastructure/Sitemap/SitemapEntry.swift b/Sources/WebUI/Core/Infrastructure/Sitemap/SitemapEntry.swift new file mode 100644 index 00000000..c926e454 --- /dev/null +++ b/Sources/WebUI/Core/Infrastructure/Sitemap/SitemapEntry.swift @@ -0,0 +1,121 @@ +import Foundation + +/// Represents a sitemap entry for a URL with metadata. +/// +/// Used to define information for a URL to be included in a sitemap.xml file. +/// Follows the Sitemap protocol as defined at https://www.sitemaps.org/protocol.html. +/// Sitemap entries enhance SEO by providing search engines with metadata about your content. +public struct SitemapEntry: Equatable, Hashable { + /// The URL of the page. + public let url: String + + /// The date when the page was last modified. + public let lastModified: Date? + + /// The expected frequency of changes to the page. + public enum ChangeFrequency: String, Equatable, Hashable, CaseIterable { + /// Content that changes every time it's accessed + case always + /// Content that changes hourly + case hourly + /// Content that changes daily + case daily + /// Content that changes weekly + case weekly + /// Content that changes monthly + case monthly + /// Content that changes yearly + case yearly + /// Content that never changes + case never + } + + /// How frequently the page is likely to change. + public let changeFrequency: ChangeFrequency? + + /// The priority of this URL relative to other URLs on your site (0.0 to 1.0). + public let priority: Double? + + /// Creates a new sitemap entry for a URL. + /// + /// - Parameters: + /// - url: The URL of the page. + /// - lastModified: The date when the page was last modified. + /// - changeFrequency: How frequently the page is likely to change. + /// - priority: The priority of this URL relative to other URLs (0.0 to 1.0). + /// + /// - Example: + /// ```swift + /// let homepage = SitemapEntry( + /// url: "https://example.com/", + /// lastModified: Date(), + /// changeFrequency: .weekly, + /// priority: 1.0 + /// ) + /// ``` + public init( + url: String, + lastModified: Date? = nil, + changeFrequency: ChangeFrequency? = nil, + priority: Double? = nil + ) { + self.url = url + self.lastModified = lastModified + self.changeFrequency = changeFrequency + + // Ensure priority is within valid range + if let priority = priority { + self.priority = min(1.0, max(0.0, priority)) + } else { + self.priority = nil + } + } + + /// Creates a homepage sitemap entry with recommended settings. + /// + /// - Parameters: + /// - baseURL: The base URL of the website. + /// - lastModified: The date when the homepage was last modified. + /// - Returns: A sitemap entry configured for a homepage. + /// + /// - Example: + /// ```swift + /// let homepage = SitemapEntry.homepage(baseURL: "https://example.com") + /// ``` + public static func homepage(baseURL: String, lastModified: Date? = nil) -> SitemapEntry { + SitemapEntry( + url: baseURL, + lastModified: lastModified, + changeFrequency: .weekly, + priority: 1.0 + ) + } + + /// Creates a content page sitemap entry with recommended settings. + /// + /// - Parameters: + /// - url: The URL of the content page. + /// - lastModified: The date when the page was last modified. + /// - isSection: Whether the page is a section page (defaults to false). + /// - Returns: A sitemap entry configured for a content page. + /// + /// - Example: + /// ```swift + /// let blogPost = SitemapEntry.contentPage( + /// url: "https://example.com/blog/post-1", + /// lastModified: Date() + /// ) + /// ``` + public static func contentPage( + url: String, + lastModified: Date? = nil, + isSection: Bool = false + ) -> SitemapEntry { + SitemapEntry( + url: url, + lastModified: lastModified, + changeFrequency: isSection ? .monthly : .yearly, + priority: isSection ? 0.8 : 0.6 + ) + } +} \ No newline at end of file diff --git a/Sources/WebUI/Core/StructuredData/ArticleSchema.swift b/Sources/WebUI/Core/Infrastructure/StructuredData/ArticleSchema.swift similarity index 100% rename from Sources/WebUI/Core/StructuredData/ArticleSchema.swift rename to Sources/WebUI/Core/Infrastructure/StructuredData/ArticleSchema.swift diff --git a/Sources/WebUI/Core/StructuredData/FaqSchema.swift b/Sources/WebUI/Core/Infrastructure/StructuredData/FaqSchema.swift similarity index 100% rename from Sources/WebUI/Core/StructuredData/FaqSchema.swift rename to Sources/WebUI/Core/Infrastructure/StructuredData/FaqSchema.swift diff --git a/Sources/WebUI/Core/StructuredData/OrganizationSchema.swift b/Sources/WebUI/Core/Infrastructure/StructuredData/OrganizationSchema.swift similarity index 100% rename from Sources/WebUI/Core/StructuredData/OrganizationSchema.swift rename to Sources/WebUI/Core/Infrastructure/StructuredData/OrganizationSchema.swift diff --git a/Sources/WebUI/Core/StructuredData/PersonSchema.swift b/Sources/WebUI/Core/Infrastructure/StructuredData/PersonSchema.swift similarity index 100% rename from Sources/WebUI/Core/StructuredData/PersonSchema.swift rename to Sources/WebUI/Core/Infrastructure/StructuredData/PersonSchema.swift diff --git a/Sources/WebUI/Core/StructuredData/ProductSchema.swift b/Sources/WebUI/Core/Infrastructure/StructuredData/ProductSchema.swift similarity index 100% rename from Sources/WebUI/Core/StructuredData/ProductSchema.swift rename to Sources/WebUI/Core/Infrastructure/StructuredData/ProductSchema.swift diff --git a/Sources/WebUI/Core/StructuredData/SchemaType.swift b/Sources/WebUI/Core/Infrastructure/StructuredData/SchemaType.swift similarity index 100% rename from Sources/WebUI/Core/StructuredData/SchemaType.swift rename to Sources/WebUI/Core/Infrastructure/StructuredData/SchemaType.swift diff --git a/Sources/WebUI/Core/StructuredData/StructuredData.swift b/Sources/WebUI/Core/Infrastructure/StructuredData/StructuredData.swift similarity index 100% rename from Sources/WebUI/Core/StructuredData/StructuredData.swift rename to Sources/WebUI/Core/Infrastructure/StructuredData/StructuredData.swift diff --git a/Sources/WebUI/Core/Website.swift b/Sources/WebUI/Core/Infrastructure/Website.swift similarity index 100% rename from Sources/WebUI/Core/Website.swift rename to Sources/WebUI/Core/Infrastructure/Website.swift diff --git a/Sources/WebUI/Core/SEO.swift b/Sources/WebUI/Core/SEO.swift deleted file mode 100644 index ba3b5685..00000000 --- a/Sources/WebUI/Core/SEO.swift +++ /dev/null @@ -1,250 +0,0 @@ -import Foundation -import Logging - -/// Represents a rule in a robots.txt file. -/// -/// Used to define instructions for web crawlers about which parts of the website should be crawled. -public struct RobotsRule { - /// The user agent the rule applies to (e.g., "Googlebot" or "*" for all crawlers). - public let userAgent: String - - /// Paths that should not be crawled. - public let disallow: [String]? - - /// Paths that are allowed to be crawled (overrides disallow rules). - public let allow: [String]? - - /// The delay between successive crawls in seconds. - public let crawlDelay: Int? - - /// Creates a new robots.txt rule. - /// - /// - Parameters: - /// - userAgent: The user agent the rule applies to (e.g., "Googlebot" or "*" for all crawlers). - /// - disallow: Paths that should not be crawled. - /// - allow: Paths that are allowed to be crawled (overrides disallow rules). - /// - crawlDelay: The delay between successive crawls in seconds. - /// - /// - Example: - /// ```swift - /// let rule = RobotsRule( - /// userAgent: "*", - /// disallow: ["/admin/", "/private/"], - /// allow: ["/public/"], - /// crawlDelay: 10 - /// ) - /// ``` - public init( - userAgent: String, - disallow: [String]? = nil, - allow: [String]? = nil, - crawlDelay: Int? = nil - ) { - self.userAgent = userAgent - self.disallow = disallow - self.allow = allow - self.crawlDelay = crawlDelay - } -} - -/// Represents a sitemap entry for a URL with metadata. -/// -/// Used to define information for a URL to be included in a sitemap.xml file. -/// Follows the Sitemap protocol as defined at https://www.sitemaps.org/protocol.html. -public struct SitemapEntry { - /// The URL of the page. - public let url: String - - /// The date when the page was last modified. - public let lastModified: Date? - - /// The expected frequency of changes to the page. - public enum ChangeFrequency: String { - case always, hourly, daily, weekly, monthly, yearly, never - } - - /// How frequently the page is likely to change. - public let changeFrequency: ChangeFrequency? - - /// The priority of this URL relative to other URLs on your site (0.0 to 1.0). - public let priority: Double? - - /// Creates a new sitemap entry for a URL. - /// - /// - Parameters: - /// - url: The URL of the page. - /// - lastModified: The date when the page was last modified. - /// - changeFrequency: How frequently the page is likely to change. - /// - priority: The priority of this URL relative to other URLs (0.0 to 1.0). - /// - /// - Example: - /// ```swift - /// let homepage = SitemapEntry( - /// url: "https://example.com/", - /// lastModified: Date(), - /// changeFrequency: .weekly, - /// priority: 1.0 - /// ) - /// ``` - public init( - url: String, - lastModified: Date? = nil, - changeFrequency: ChangeFrequency? = nil, - priority: Double? = nil - ) { - self.url = url - self.lastModified = lastModified - self.changeFrequency = changeFrequency - self.priority = priority - } -} - -/// Utility functions for generating SEO-related files for a website. -/// -/// This class provides methods for generating sitemap.xml and robots.txt files, -/// which are important for search engine optimization (SEO) and web crawler control. -public struct SEOUtils { - private static let logger = Logger(label: "com.webui.seo.utils") - - /// Generates a sitemap.xml file content from website routes and custom entries. - /// - /// This method creates a standard sitemap.xml file that includes all routes in the website, - /// plus any additional custom sitemap entries. It follows the Sitemap protocol specification - /// from sitemaps.org. - /// - /// - Parameters: - /// - baseURL: The base URL of the website (e.g., "https://example.com"). - /// - routes: The document routes in the website. - /// - customEntries: Additional sitemap entries to include. - /// - Returns: A string containing the XML content of the sitemap. - /// - /// - Note: If any route has a `metadata.date` value, it will be used as the lastmod date - /// in the sitemap. Priority is set based on the path depth (home page gets 1.0). - public static func generateSitemapXML( - baseURL: String, - routes: [Document], - customEntries: [SitemapEntry]? - ) -> String { - let dateFormatter = ISO8601DateFormatter() - - var xml = """ - - - - """ - - // Add entries for all routes - for route in routes { - let path = route.path ?? "index" - let url = "\(baseURL)/\(path == "index" ? "" : "\(path).html")" - - var entry = " \n \(url)\n" - - // Add lastmod if metadata has a date - if let date = route.metadata.date { - entry += " \(dateFormatter.string(from: date))\n" - } - - // Set priority based on path depth (home page gets higher priority) - let depth = path.components(separatedBy: "/").count - let priority = path == "index" ? 1.0 : max(0.5, 1.0 - Double(depth) * 0.1) - entry += " \(String(format: "%.1f", priority))\n" - - // Add changefreq based on path (index and main sections change more frequently) - if path == "index" { - entry += " weekly\n" - } else if depth == 1 { - entry += " monthly\n" - } else { - entry += " yearly\n" - } - - entry += " \n" - xml += entry - } - - // Add custom sitemap entries - if let customEntries = customEntries { - for entry in customEntries { - xml += " \n \(entry.url)\n" - - if let lastMod = entry.lastModified { - xml += " \(dateFormatter.string(from: lastMod))\n" - } - - if let changeFreq = entry.changeFrequency { - xml += " \(changeFreq.rawValue)\n" - } - - if let priority = entry.priority { - xml += " \(String(format: "%.1f", priority))\n" - } - - xml += " \n" - } - } - - xml += "" - return xml - } - - /// Generates a robots.txt file content. - /// - /// This method creates a standard robots.txt file that includes instructions for web crawlers, - /// including a reference to the sitemap if one exists. - /// - /// - Parameters: - /// - baseURL: The optional base URL of the website (e.g., "https://example.com"). - /// - generateSitemap: Whether a sitemap is being generated for this website. - /// - robotsRules: Custom rules to include in the robots.txt file. - /// - Returns: A string containing the content of the robots.txt file. - /// - /// - Note: If custom rules are provided, they will be included in the file. - /// Otherwise, a default permissive robots.txt will be generated. - public static func generateRobotsTxt( - baseURL: String?, - generateSitemap: Bool, - robotsRules: [RobotsRule]? - ) -> String { - var content = "# robots.txt generated by WebUI\n\n" - - // Add custom rules if provided - if let rules = robotsRules, !rules.isEmpty { - for rule in rules { - // Add user-agent section - content += "User-agent: \(rule.userAgent)\n" - - // Add disallow paths - if let disallow = rule.disallow, !disallow.isEmpty { - for path in disallow { - content += "Disallow: \(path)\n" - } - } - - // Add allow paths - if let allow = rule.allow, !allow.isEmpty { - for path in allow { - content += "Allow: \(path)\n" - } - } - - // Add crawl delay if provided - if let crawlDelay = rule.crawlDelay { - content += "Crawl-delay: \(crawlDelay)\n" - } - - content += "\n" - } - } else { - // Default permissive robots.txt - content += "User-agent: *\nAllow: /\n\n" - } - - // Add sitemap reference if sitemap is generated and baseURL is provided - if generateSitemap, let baseURL = baseURL { - content += "Sitemap: \(baseURL)/sitemap.xml\n" - } - - return content - } -} diff --git a/Sources/WebUI/Core/Theme.swift b/Sources/WebUI/Core/Theme/Theme.swift similarity index 100% rename from Sources/WebUI/Core/Theme.swift rename to Sources/WebUI/Core/Theme/Theme.swift diff --git a/Sources/WebUI/Documentation.docc/CoreConcepts.md b/Sources/WebUI/Documentation.docc/CoreConcepts.md new file mode 100644 index 00000000..bc945d90 --- /dev/null +++ b/Sources/WebUI/Documentation.docc/CoreConcepts.md @@ -0,0 +1,139 @@ +# Core Concepts + +Learn about the fundamental building blocks and architecture of WebUI. + +## Overview + +WebUI is built around several key concepts that work together to create static websites: + +- Documents: The foundation of each page +- Elements: Type-safe HTML components +- Themes: Consistent styling across your site +- Metadata: SEO and social sharing optimization + +## Documents + +A Document represents a single page in your website. It handles: + +- HTML structure +- Metadata and SEO +- Scripts and stylesheets +- Content organization + +```swift +Document( + path: "about", + metadata: Metadata( + title: "About Us", + description: "Learn about our team" + ), + scripts: [ + Script(src: "/scripts/main.js", attribute: .defer) + ], + stylesheets: [ + "/styles/main.css" + ], + head: """ + + """ +) { + Article { + Heading(.title) { "About Us" } + Text { "Our story..." } + } +} +``` + +## Elements + +Elements are the building blocks of your pages. WebUI provides type-safe wrappers around HTML elements: + +```swift +Header { + Text { "Logo" } + Navigation { + Link(to: "home") { "Home" } + Link(to: "about") { "About" } + Link(to: "https://example.com", newTab: true) { "External" } + } +} + +Main { + Stack { + Heading(.largeTitle) { "Welcome" } + Text { "Main content here" } + } +} +``` + +## Theme System + +The Theme system allows you to define consistent design tokens: + +```swift +let theme = Theme( + colors: [ + "primary": "blue", + "secondary": "#10b981" + ], + spacing: [ + "4": "1rem" + ], + textSizes: [ + "lg": "1.25rem" + ], + custom: [ + "opacity": ["faint": "0.1"] + ] +) +``` + +Apply the theme to your website: + +```swift +Website( + routes: [/* your routes */], + theme: theme +) +``` + +## Website Structure + +The Website type is the main container that: + +- Organizes your routes +- Handles build process +- Manages SEO features +- Configures global settings + +```swift +let app = Website( + routes: [/* your documents */], + baseURL: "https://example.com", + theme: theme, + generateSitemap: true +) + +try app.build(to: URL(fileURLWithPath: ".build")) +``` + +## Topics + +### Essentials + +- ``Document`` +- ``Element`` +- ``Website`` + +### Styling + +- ``Theme`` +- ``Style`` + +### Content + +- ``HTML`` +- ``Children`` +- ``HTMLBuilder`` diff --git a/Sources/WebUI/Documentation.docc/CustomElements.md b/Sources/WebUI/Documentation.docc/CustomElements.md new file mode 100644 index 00000000..54b79086 --- /dev/null +++ b/Sources/WebUI/Documentation.docc/CustomElements.md @@ -0,0 +1,199 @@ +# Creating Custom Elements + +Learn how to create reusable components in WebUI. + +## Overview + +WebUI allows you to create custom elements that encapsulate design and behavior. Custom elements can: +- Accept generic content +- Handle custom styling +- Manage internal state +- Be composition-friendly + +## Basic Custom Elements + +Create a simple custom element: + +```swift +struct Card: Element { + var title: String + var content: String + + var body: some HTML { + Layout.stack { + Text(title) + .font(size: .lg, weight: .bold) + Text(content) + .color(.gray(._600)) + } + .padding(.medium) + .backgroundColor(.white) + .rounded(.md) + .shadow(.medium) + } +} +``` + +Use your custom element: + +```swift +Card( + title: "Welcome", + content: "This is a custom card component" +) +``` + +## Generic Content + +Create elements that accept custom content: + +```swift +struct Container: Element { + var alignment: Alignment = .center + var maxWidth: Length = .container(.large) + var content: () -> Content + + var body: some HTML { + Layout.flex { + content() + } + .alignItems(alignment) + .maxWidth(maxWidth) + .marginX(.auto) + .padding(.medium) + } +} +``` + +Use with any content: + +```swift +Container { + Stack { + Heading(.title) { "Title" } + Text { "Content" } + } +} +``` + +## Layout Components + +Create reusable layout patterns: + +```swift +struct SplitView: Element { + var left: () -> Left + var right: () -> Right + + var body: some HTML { + Layout.grid { + Layout.flex { + left() + } + .gridColumn(.span(4)) + + Layout.flex { + right() + } + .gridColumn(.span(8)) + } + .columns(12) + .gap(.medium) + .responsive { + sm { + grid(columns: 1) + } + } + } +} +``` + +## State Management + +Create elements with internal state: + +```swift +struct Counter: Element { + @State var count: Int = 0 + + var body: some HTML { + Stack { + Text { "Count: \(count)" } + .font(size: .xl) + + Button("Increment") { + count += 1 + } + .padding() + .backgroundColor(.blue(._500)) + .color(.white) + .rounded(.md) + } + .spacing(of: 4) + } +} +``` + +## Composable Elements + +Create elements that work well together: + +```swift +struct FormField: Element { + var label: String + var error: String? + var required: Bool = false + + var body: some HTML { + Stack { + Label(label) + .font(weight: .medium) + + Input(type: .text) + .required(required) + .border(of: 1, color: error != nil ? .red(._500) : .gray(._200)) + + if let error = error { + Text(error) + .color(.red(._500)) + .font(size: .sm) + } + } + .spacing(of: 2) + } +} + +struct FormSection: Element { + var title: String + var children: [FormField] + + var body: some HTML { + Stack { + Text(title) + .font(size: .lg, weight: .semibold) + + Stack { + children + } + .spacing(of: 4) + } + .padding() + .border(of: 1, color: .gray(._200)) + .rounded(.lg) + } +} +``` + +## Topics + +### Basics + +- ``Element`` +- ``HTML`` +- ``Children`` + +### State + +- ``State`` +- ``Binding`` +- ``Observable`` diff --git a/Sources/WebUI/Documentation.docc/FormElements.md b/Sources/WebUI/Documentation.docc/FormElements.md new file mode 100644 index 00000000..4d5c1833 --- /dev/null +++ b/Sources/WebUI/Documentation.docc/FormElements.md @@ -0,0 +1,189 @@ +# Form Elements + +Learn how to create and handle forms in WebUI. + +## Overview + +WebUI provides a comprehensive set of form elements with built-in validation and styling support. Form elements are designed to be: + +- Type-safe +- Accessible +- Easy to validate +- Styleable with the WebUI styling system + +## Basic Form Usage + +Create a basic form with various input types: + +```swift +Form { + Input(type: .text, name: "username") + .placeholder("Enter username") + .required(true) + .minLength(3) + .maxLength(20) + .validationMessage("Username must be 3-20 characters") + + Input(type: .email, name: "email") + .placeholder("Enter email") + .required(true) + + TextArea(name: "message") + .placeholder("Your message") + .rows(5) + + Button("Submit") + .type(.submit) +} +.onSubmit { data in + // Handle form submission +} +``` + +## Form Layout + +Group form elements with semantic layout components: + +```swift +Form { + Stack { + Label("Personal Information") + .font(size: .lg, weight: .semibold) + + Grid { + Label("First Name") + Input(type: .text, name: "firstName") + .required(true) + + Label("Last Name") + Input(type: .text, name: "lastName") + .required(true) + } + .columns(2) + .gap(.medium) + } + .spacing(of: 4) +} +``` + +## Input Types + +WebUI supports all HTML5 input types: + +```swift +Input(type: .text) // Text input +Input(type: .email) // Email input +Input(type: .password) // Password input +Input(type: .number) // Number input +Input(type: .tel) // Telephone input +Input(type: .url) // URL input +Input(type: .date) // Date picker +Input(type: .time) // Time picker +Input(type: .file) // File upload +Input(type: .checkbox) // Checkbox +Input(type: .radio) // Radio button +``` + +## Form Validation + +Add validation rules to your inputs: + +```swift +Input(type: .text, name: "username") + .required(true) + .pattern("[A-Za-z0-9]+") + .minLength(3) + .maxLength(20) + .validationMessage("Username must be 3-20 characters, alphanumeric only") + +Input(type: .email, name: "email") + .required(true) + .validationMessage("Please enter a valid email address") + +Input(type: .number, name: "age") + .min(18) + .max(100) + .validationMessage("Age must be between 18 and 100") +``` + +## Custom Form Controls + +Create reusable form components: + +```swift +struct FormField: Element { + var label: String + var name: String + var type: Input.`Type` + var required: Bool = false + var validationMessage: String? = nil + + var body: some HTML { + Stack { + Label(label) + .font(weight: .medium) + Input(type: type, name: name) + .required(required) + .validationMessage(validationMessage) + } + .spacing(of: 2) + } +} +``` + +Use your custom components: + +```swift +Form { + FormField( + label: "Username", + name: "username", + type: .text, + required: true, + validationMessage: "Username is required" + ) + FormField( + label: "Email", + name: "email", + type: .email, + required: true + ) +} +``` + +## Styling Forms + +Apply WebUI's styling system to form elements: + +```swift +Input(type: .text, name: "search") + .padding(of: 3) + .rounded(.md) + .border(of: 1, color: .gray(._200)) + .on(.focus) { + border(of: 2, color: .blue(._500)) + outline(width: 0) + } +``` + +## Topics + +### Form Elements + +- ``Form`` +- ``Input`` +- ``TextArea`` +- ``Label`` +- ``Button`` + +### Validation + +- ``ValidationRule`` +- ``ValidationMessage`` +- ``FormValidation`` + +### Events + +- ``FormEvent`` +- ``SubmitHandler`` +- ``InputHandler`` diff --git a/Sources/WebUI/Documentation.docc/GettingStarted.md b/Sources/WebUI/Documentation.docc/GettingStarted.md new file mode 100644 index 00000000..de0e12fe --- /dev/null +++ b/Sources/WebUI/Documentation.docc/GettingStarted.md @@ -0,0 +1,134 @@ +# Getting Started with WebUI + +Learn how to create static websites using WebUI's type-safe Swift API. + +## Overview + +WebUI is a static site generator that leverages Swift's type system to create maintainable and SEO-friendly websites. This guide will help you get started with WebUI. + +## Installation + +Add WebUI to your project using Swift Package Manager: + +```swift +dependencies: [ + .package(url: "https://github.com/maclong9/web-ui.git", from: "1.0.0") +] +``` + +## Basic Example + +Here's a simple website with two pages: + +```swift +import WebUI + +// Create a basic website with an index page +let app = Website( + routes: [ + Document( + path: "index", + metadata: .init( + title: "Home", + description: "Welcome to my website" + ) + ) { + Header { + Text { "Logo" } + Navigation { + Link(to: "about") { "About" } + } + } + Main { + Stack { + Heading(.largeTitle) { "Welcome" } + Text { "Built with WebUI" } + } + } + }, + Document( + path: "about", + metadata: .init( + title: "About", + description: "Learn more about us" + ) + ) { + Article { + Heading(.title) { "About Us" } + Text { "Our story begins here..." } + } + } + ] +) + +// Build the website to the output directory +try app.build(to: URL(fileURLWithPath: ".build")) +``` + +## Adding SEO Features + +WebUI provides comprehensive SEO support: + +```swift +let app = Website( + routes: [/* your routes */], + baseURL: "https://example.com", + // Generate sitemap.xml + sitemapEntries: [ + SitemapEntry( + url: "https://example.com/custom", + lastModified: Date(), + changeFrequency: .monthly, + priority: 0.8 + ) + ], + // Configure robots.txt + robotsRules: [ + RobotsRule( + userAgent: "*", + disallow: ["/private/"], + allow: ["/public/"], + crawlDelay: 10 + ) + ] +) +``` + +## Metadata and Favicons + +Add rich metadata and favicon support: + +```swift +Document( + path: "index", + metadata: Metadata( + site: "My Site", + title: "Welcome", + description: "A beautiful website built with WebUI", + image: "/images/og.jpg", + author: "Jane Doe", + keywords: ["swift", "web", "ui"], + twitter: "@handle", + locale: .en, + type: .website, + favicons: [ + Favicon("/favicon-32.png", dark: "/favicon-dark-32.png", size: "32x32"), + Favicon("/favicon.ico", type: "image/x-icon") + ] + ) +) { + // Your content here +} +``` + +## Topics + +### Basics + +- +- + +### Advanced Features + +- +- diff --git a/Sources/WebUI/Documentation.docc/ResponsiveDesign.md b/Sources/WebUI/Documentation.docc/ResponsiveDesign.md new file mode 100644 index 00000000..71201385 --- /dev/null +++ b/Sources/WebUI/Documentation.docc/ResponsiveDesign.md @@ -0,0 +1,175 @@ +# Responsive Design + +Learn how to create responsive layouts using WebUI's responsive design system. + +## Overview + +WebUI provides a powerful responsive design system that allows you to: +- Define styles for different screen sizes +- Create mobile-first layouts +- Use breakpoint-specific styling +- Combine multiple responsive modifiers + +## Basic Responsive Styling + +Use the `on` or `responsive` modifier to apply breakpoint-specific styles: + +```swift +Button("Click Me") + .font(size: .sm) + .responsive { + md { + font(size: .lg) + } + } +``` + +## Multiple Breakpoints + +Apply styles across different breakpoints: + +```swift +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)) + } + } +``` + +## Responsive Layout + +Create layouts that adapt to screen size: + +```swift +Layout.flex { + Text("Sidebar") + Text("Main Content") +} +.responsive { context in + sm { + direction(.column) + } + md { + direction(.row) + } +} +``` + +## Grid System + +Use the responsive grid system: + +```swift +Layout.grid { + Text("Header") + .gridColumn(.span(12)) + Text("Sidebar") + .gridColumn(.span(3)) + Text("Content") + .gridColumn(.span(9)) +} +.columns(12) +.gap(.medium) +.on { + sm { + grid(columns: 1) + } + lg { + grid(columns: 12) + } +} +``` + +## Breakpoint-Specific Visibility + +Control element visibility at different breakpoints: + +```swift +Element(tag: "div") + .hidden() + .on { + md { + hidden(false) + } + } +``` + +## Complex Components + +Create components with comprehensive responsive behavior: + +```swift +struct ResponsiveNavigation: Element { + var body: some HTML { + Navigation { + // Mobile menu button + Button("Menu") + .hidden() + .on { + sm { + hidden(false) + } + md { + hidden(true) + } + } + + // Navigation links + Layout.flex { + Link(to: "home") { "Home" } + Link(to: "about") { "About" } + Link(to: "contact") { "Contact" } + } + .direction(.column) + .on { + md { + direction(.row) + gap(.medium) + } + } + } + } +} +``` + +## Available Breakpoints + +| Breakpoint | Screen Size | +|------------|-------------| +| `xs` | Extra small | +| `sm` | Small | +| `md` | Medium | +| `lg` | Large | +| `xl` | Extra large | +| `xl2` | 2X Large | + +## Topics + +### Basics + +- ``Responsive`` +- ``Breakpoint`` +- ``ResponsiveModifier`` + +### Layout + +- ``ResponsiveLayout`` +- ``ResponsiveGrid`` +- ``ResponsiveFlex`` + +### Utilities + +- ``BreakpointContext`` +- ``ResponsiveValue`` +- ``ViewportSize`` \ No newline at end of file diff --git a/Sources/WebUI/Documentation.docc/SEOGuide.md b/Sources/WebUI/Documentation.docc/SEOGuide.md new file mode 100644 index 00000000..a66804b7 --- /dev/null +++ b/Sources/WebUI/Documentation.docc/SEOGuide.md @@ -0,0 +1,179 @@ +# SEO and Metadata in WebUI + +Learn how to optimize your WebUI websites for search engines and social sharing. + +## Overview + +WebUI provides comprehensive SEO tools including: +- Metadata management +- Structured data (Schema.org) +- Automatic sitemap generation +- Robots.txt configuration +- Social media optimization + +## Basic Metadata + +```swift +let metadata = Metadata( + site: "My Website", + title: "Welcome", + titleSeperator: " | ", + description: "A fantastic website built with WebUI", + date: Date(), + image: "/images/og.jpg", + author: "Jane Doe", + keywords: ["swift", "web", "static site"], + twitter: "twitterhandle", + locale: .en, + type: .website +) +``` + +### Available Locales + +WebUI supports numerous locales including: +- European: `.en`, `.es`, `.fr`, `.de`, `.it`, `.nl` +- Asian: `.ja`, `.zhCN`, `.zhTW`, `.ko` +- Middle Eastern: `.ar`, `.he`, `.tr` +- And many more + +### Content Types + +Available content types: +- `.website` +- `.article` +- `.video` +- `.profile` + +## Structured Data + +WebUI supports various Schema.org structured data types: + +### Article Schema + +```swift +let articleData = StructuredData.article( + headline: "My Article", + image: "https://example.com/image.jpg", + author: "Jane Doe", + publisher: "My Publication", + datePublished: Date(), + description: "An interesting article" +) +``` + +### Product Schema + +```swift +let productData = StructuredData.product( + name: "Amazing Product", + image: "https://example.com/product.jpg", + description: "Best product ever", + sku: "PROD-123", + brand: "My Brand", + offers: [ + "price": "99.99", + "priceCurrency": "USD", + "availability": "InStock" + ] +) +``` + +### Person Schema + +```swift +let personData = StructuredData.person( + name: "Jane Doe", + givenName: "Jane", + familyName: "Doe", + image: "https://example.com/jane.jpg", + jobTitle: "Software Engineer", + email: "jane@example.com", + telephone: "+1-555-123-4567", + url: "https://janedoe.example.com", + address: [ + "streetAddress": "123 Main St", + "addressLocality": "Anytown", + "postalCode": "12345", + "addressCountry": "US" + ] +) +``` + +## Sitemap Generation + +Configure sitemap generation with custom entries: + +```swift +let app = Website( + routes: [/* your routes */], + baseURL: "https://example.com", + sitemapEntries: [ + SitemapEntry( + url: "https://example.com/custom", + lastModified: Date(), + changeFrequency: .monthly, + priority: 0.8 + ) + ] +) +``` + +## Robots.txt Configuration + +Customize your robots.txt file: + +```swift +let app = Website( + routes: [/* your routes */], + baseURL: "https://example.com", + robotsRules: [ + RobotsRule( + userAgent: "*", + disallow: ["/private/", "/admin/"], + allow: ["/public/"], + crawlDelay: 10 + ), + RobotsRule( + userAgent: "Googlebot", + disallow: ["/nogoogle/"] + ) + ] +) +``` + +## Favicon Support + +Add comprehensive favicon support with dark mode: + +```swift +let metadata = Metadata( + // ... other metadata ... + favicons: [ + Favicon("/favicon-32.png", dark: "/favicon-dark-32.png", size: "32x32"), + Favicon("/favicon-16.png", size: "16x16"), + Favicon("/favicon.ico", type: "image/x-icon") + ] +) +``` + +## Topics + +### Basics + +- ``Metadata`` +- ``MetadataComponents`` +- ``MetadataTypes`` + +### Structured Data + +- ``StructuredData`` +- ``ArticleSchema`` +- ``ProductSchema`` +- ``PersonSchema`` + +### SEO Tools + +- ``SitemapEntry`` +- ``RobotsRule`` +- ``Favicon`` diff --git a/Sources/WebUI/Documentation.docc/StylingGuide.md b/Sources/WebUI/Documentation.docc/StylingGuide.md new file mode 100644 index 00000000..e223c900 --- /dev/null +++ b/Sources/WebUI/Documentation.docc/StylingGuide.md @@ -0,0 +1,176 @@ +# Styling in WebUI + +Learn how to style your web applications using WebUI's type-safe styling system. + +## Overview + +WebUI provides a comprehensive styling system that combines Swift's type safety with modern CSS capabilities. The styling system includes: + +- Responsive design with breakpoints +- Typography and spacing +- Layout controls +- Sizing and positioning +- Interactive states + +## Basic Styling + +### Frame and Sizing + +Control element dimensions using the `frame` modifier: + +```swift +Element(tag: "div") + .frame(width: .full, height: .screen) // Full width, screen height + .frame(width: .fraction(1, 2)) // Half width + .frame(width: .container(.medium)) // Container width + .frame(width: .character(60)) // Character-based width + .frame(minWidth: .min, maxHeight: .fit) // Min/max constraints +``` + +### Typography + +Style text with the `font` modifier: + +```swift +Text("Hello World") + .font( + size: .xl, + weight: .semibold, + color: .gray(._700) + ) + .font( + alignment: .center, + tracking: .wide, + leading: .loose + ) +``` + +### Spacing + +Control margins and padding: + +```swift +Element(tag: "div") + .margins(of: 4) // All sides + .margins(of: 8, at: .top, .bottom) // Specific edges + .margins(at: .horizontal, auto: true) // Auto margins + .padding(of: 6) // Padding all sides + .padding(of: 5, at: .vertical) // Vertical padding + .spacing(of: 4, along: .both) // Child element spacing +``` + +## Responsive Design + +WebUI provides a powerful responsive design system using the `responsive` or `on` modifier: + +```swift +Button("Click Me") + .background(color: .blue(._500)) + .padding(of: 2) + .on { + sm { + padding(of: 3) + } + md { + padding(of: 4) + font(size: .lg) + } + lg { + padding(of: 6) + background(color: .blue(._600)) + } + } +``` + +### Available Breakpoints + +- `xs`: Extra small screens +- `sm`: Small screens +- `md`: Medium screens +- `lg`: Large screens +- `xl`: Extra large screens +- `xl2`: 2X Extra large screens + +## Layout + +### Flex Layout + +```swift +Layout.flex { + Text("Sidebar") + .width(250) + Text("Main Content") + .flexGrow(1) +} +.gap(.medium) +``` + +### Grid Layout + +```swift +Layout.grid { + Text("Header") + .gridColumn(.span(12)) + Text("Sidebar") + .gridColumn(.span(3)) + Text("Content") + .gridColumn(.span(9)) +} +.columns(12) +.gap(.medium) +``` + +## Interactive States + +Add styles for different interaction states: + +```swift +Button("Submit") + .background(color: .blue(._500)) + .on(.hover) { + background(color: .blue(._600)) + } + .on(.focus) { + outline(width: 2, color: .blue(._300)) + } +``` + +## Custom Themes + +Define custom themes with consistent design tokens: + +```swift +let theme = Theme( + colors: [ + "primary": "blue", + "secondary": "#10b981" + ], + spacing: ["4": "1rem"], + textSizes: ["lg": "1.25rem"], + custom: ["opacity": ["faint": "0.1"]] +) +``` + +## Topics + +### Basics + +- ``Style`` +- ``Theme`` +- ``Color`` + +### Layout + +- ``Layout`` +- ``Flex`` +- ``Grid`` + +### Responsive + +- ``Breakpoint`` +- ``ResponsiveModifier`` + +### Advanced + +- ``CustomStyle`` +- ``StyleModifier`` diff --git a/Sources/WebUI/Documentation.docc/WebUI.md b/Sources/WebUI/Documentation.docc/WebUI.md index 56ac94a0..43f0f869 100644 --- a/Sources/WebUI/Documentation.docc/WebUI.md +++ b/Sources/WebUI/Documentation.docc/WebUI.md @@ -1,79 +1,77 @@ # ``WebUI`` -Create beautiful static and dynamic websites using Swift. +A Swift framework for building static websites with a type-safe, declarative syntax. ## Overview -WebUI is a Swift library for creating websites with a declarative, component-based approach. It allows you to write HTML using Swift's type-safe syntax, with built-in support for styling, accessibility, and optimization. You can use WebUI for both static site generation and dynamic server-rendered content. - -The library is built on a modular architecture that organizes functionality into focused components, making it easy to use only what you need and to extend the library in a consistent way. - -With WebUI, you can: -- Build type-safe HTML using Swift -- Apply styling using a Tailwind CSS-inspired API -- Create reusable components -- Generate static sites with optimized markup -- Create dynamic server-rendered content -- Add Markdown content with front matter support +WebUI enables you to build static websites using Swift with a powerful, type-safe API that generates clean HTML. It provides built-in support for: + +- SEO optimization with metadata and structured data +- Responsive design with type-safe styling +- Component-based architecture +- Built-in sitemap and robots.txt generation +- Theme customization +- Favicons and metadata handling + +## Basic Usage + +```swift +let app = Website( + routes: [ + Document( + path: "index", + metadata: .init( + title: "Hello", + description: "Welcome to my site" + ) + ) { + Header { + Text { "Logo" } + Navigation { + Link(to: "about") { "About" } + } + } + Main { + Stack { + Heading(.largeTitle) { "Welcome" } + Text { "Built with WebUI" } + } + } + } + ] +) + +try app.build(to: URL(fileURLWithPath: "build")) +``` ## Topics ### Essentials -- -- -- - -### Tutorials - -- +- +- -### Creating Content +### Website Structure - ``Document`` - ``Website`` -- ``Element`` -- ``HTMLBuilder`` - -### Building Elements - -- ``Text`` -- ``Heading`` -- ``Link`` -- ``Button`` -- ``Image`` -- ``List`` -- ``Stack`` -- ``Fragment`` - -### Layout - -- ``Header`` -- ``Main`` -- ``Footer`` -- ``Navigation`` -- ``Section`` -- ``Article`` -- ``Aside`` +- ``Theme`` -### Styling - -- ``Color`` -- ``BorderStyle`` -- ``Modifier`` -- ``Edge`` -- ``ResponsiveBuilder`` +### Metadata and SEO -### Responsive Design +- ``Metadata`` +- ``StructuredData`` +- ``SitemapEntry`` +- ``RobotsRule`` -- -- ``Element/on(_:)`` -- ``ResponsiveBuilder`` - -### Tutorials +### Styling -- +- +- ``Style`` +- -### Examples +### Advanced Topics -- [Portfolio](https://github.com/maclong9/portfolio) - A personal portfolio website built with WebUI +- +- ``HTML`` +- ``Children`` diff --git a/Sources/WebUI/Documentation.docc/getting-started.md b/Sources/WebUI/Documentation.docc/getting-started.md deleted file mode 100644 index 59400079..00000000 --- a/Sources/WebUI/Documentation.docc/getting-started.md +++ /dev/null @@ -1,123 +0,0 @@ -# Getting Started with WebUI - -Create static and dynamic websites using Swift with WebUI's component-based, type-safe API. - -## Overview - -WebUI is a Swift library for building websites using a declarative, component-based approach. It offers a type-safe way to create HTML, with built-in styling inspired by SwiftUI, and all in pure Swift. WebUI can be used for both static site generation and dynamic server-rendered content. - -This article covers the basics of getting started with WebUI, from installation to creating your first page. - -## Adding WebUI to Your Project - -Add WebUI to your Swift package by adding it as a dependency in your `Package.swift` file: - -```swift -// swift-tools-version: 6.1 -import PackageDescription - -let package = Package( - name: "MyWebsite", - platforms: [.macOS(.v15)], - dependencies: [ - .package(url: "https://github.com/maclong9/web-ui.git", from: "1.0.0"), - ], - targets: [ - .executableTarget( - name: "MyWebsite", - dependencies: [ - .product(name: "WebUI", package: "web-ui") - ], - path: "Sources", - resources: [.process("Public")] // Add your asset files here - ) - ] -) -``` - -> You can also create a site as part of a monorepo project by removing the `path` from the target and creating the directory structure for your site as `Sources/TargetName/*`. - -## Creating Your First Page - -To create a basic web page with WebUI, you need three key components: - -1. **Metadata**: Information about the page like title and description -2. **Content**: The actual HTML elements and text for the page -3. **Document**: The container that brings metadata and content together - -Here's a simple example: - -```swift -import WebUI - -let homePage = Document( - path: "index", - metadata: Metadata( - title: "My First Page", - description: "A simple page built with WebUI" - ), - content: { - Header { - Heading(.largeTitle) { "Welcome to My Website" } - } - - Main { - Text { "This is my first page built with WebUI. It's so easy!" } - - Link(to: "https://github.com") { "Visit GitHub" } - } - - Footer { - Text { "© 2025 My Website" } - } - } -) -``` - -## Building a Complete Website - -To create a complete website with multiple pages, you'll use the ``Website`` struct, the ``Metadata`` here is applied to all pages: - -```swift -import WebUI - -let website = Website( - metadata: Metadata( - site: "My Website", - description: "A website built with WebUI" - ), - routes: [ - homePage, - aboutPage, - contactPage - ] -) - -// Build the static site -try website.build(to: URL(filePath: ".output")) // `.output` is the default value -``` - -## Styling Elements - -WebUI provides a SwiftUI like modifier API for styling your elements: - -```swift -Button(type: .submit) { "Submit" } - .background(color: .blue(.500)) - .font(color: .white) - .padding(of: 4, at: .all) - .rounded(.md) - .font(weight: .semibold) -``` - - -Button(type: .submit) { "Submit" } - .background(color: .blue(.500)) - - - -## Next Steps - -Now that you understand the basics of WebUI: - -- Try the tutorial for a full look at the recommended way to structure your website, pages and components. diff --git a/Sources/WebUI/Documentation.docc/styles.md b/Sources/WebUI/Documentation.docc/styles.md deleted file mode 100644 index bce25ead..00000000 --- a/Sources/WebUI/Documentation.docc/styles.md +++ /dev/null @@ -1,107 +0,0 @@ -# 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/Documentation.docc/table-of-contents.tutorial b/Sources/WebUI/Documentation.docc/table-of-contents.tutorial deleted file mode 100644 index 9a196c79..00000000 --- a/Sources/WebUI/Documentation.docc/table-of-contents.tutorial +++ /dev/null @@ -1,13 +0,0 @@ -@Tutorials(name: "WebUI") { - @Intro(title: "Learning WebUI") { - WebUI is a Swift library for creating websites with a declarative, component-based approach. Follow these tutorials to learn how to build beautiful static and dynamic websites with Swift. - } - - @Chapter(name: "Building Static Sites") { - Create static websites with multiple pages using the WebUI library. - - @Image(source: intro-image.png, alt: "Screenshot of a WebUI static site") - - @TutorialReference(tutorial: "doc:creating-a-static-site") - } -} diff --git a/Sources/WebUI/Elements/Base/List.swift b/Sources/WebUI/Elements/Base/List.swift deleted file mode 100644 index 9b780996..00000000 --- a/Sources/WebUI/Elements/Base/List.swift +++ /dev/null @@ -1,147 +0,0 @@ -/// Defines types of HTML list elements. -/// -/// HTML supports two main types of lists: ordered (numbered) lists and unordered (bulleted) lists. -/// This enum provides a type-safe way to specify which list type to create. -public enum ListType: String { - /// Creates an ordered (numbered) list using the `
    ` tag. - /// - /// Use for sequential, prioritized, or step-by-step items. - case ordered = "ol" - - /// Creates an unordered (bulleted) list using the `
      ` tag. - /// - /// Use for items where the sequence doesn't matter. - case unordered = "ul" -} - -/// Defines styles for HTML list elements. -/// -/// HTML supports various styles for list elements, such as disc, circle, or square. -/// This enum provides a type-safe way to specify which style to use. -public enum ListStyle: String { - /// Creates a list with no bullets or numbers. - case none = "" - - /// Creates a list with bullets shaped like discs. - case disc - - /// Creates a list with bullets shaped like circles. - case circle - - /// Creates a list with bullets shaped like squares. - case square = "[square]" -} - -/// Generates HTML list elements (`
        ` or `
          `). -/// -/// `List` can be rendered as an unordered list with bullet points when sequence is unimportant, -/// or as an ordered list with numbered markings when sequence matters. -/// -/// - Note: Use `Item` elements as children of a `List` to create list items. -/// -/// ## Example -/// ```swift -/// List(type: .ordered) { -/// Item { "First item" } -/// Item { "Second item" } -/// Item { "Third item" } -/// } -/// // Renders:
          1. First item
          2. Second item
          3. Third item
          -/// ``` -public final class List: Element { - let type: ListType - let style: ListStyle - - /// Creates a new HTML list element (`
            ` or `
              `). - /// - /// - Parameters: - /// - type: List type (ordered or unordered), defaults to unordered. - /// - style: List style (disc, circle, or square), defaults to none. - /// - id: Unique identifier for the HTML element. - /// - classes: An array of CSS classnames for styling the list. - /// - role: ARIA role of the element for accessibility. - /// - label: ARIA label to describe the element for screen readers. - /// - data: Dictionary of `data-*` attributes for storing custom data. - /// - content: Closure providing list items, typically `Item` elements. - /// - /// ## Example - /// ```swift - /// List(type: .unordered, classes: ["checklist"]) { - /// Item { "Buy groceries" } - /// Item { "Clean house" } - /// } - /// ``` - public init( - type: ListType = .unordered, - style: ListStyle = .none, - id: String? = nil, - classes: [String]? = nil, - role: AriaRole? = nil, - label: String? = nil, - data: [String: String]? = nil, - @HTMLBuilder content: @escaping () -> [any HTML] = { [] } - ) { - self.type = type - self.style = style - super.init( - tag: type.rawValue, - id: id, - classes: (classes ?? []) + (style != .none ? ["list-\(style.rawValue)"] : []), - role: role, - label: label, - data: data, - content: content - ) - } -} - -/// Generates an HTML list item element (`
            1. `). -/// -/// `Item` elements should be used as children of a `List` element to represent -/// individual entries in a list. Each item can contain any HTML content. -/// -/// ## Example -/// ```swift -/// Item { -/// Text { "This is a list item with " } -/// Strong { "bold text" } -/// } -/// // Renders:
            2. This is a list item with bold text
            3. -/// ``` -public final class Item: Element { - /// Creates a new HTML list item element. - /// - /// - Parameters: - /// - id: Unique identifier for the HTML element. - /// - classes: An array of CSS classnames for styling the list item. - /// - role: ARIA role of the element for accessibility. - /// - label: ARIA label to describe the element for screen readers. - /// - data: Dictionary of `data-*` attributes for storing custom data. - /// - content: Closure providing the list item's content (text or other HTML elements). - /// - /// ## Example - /// ```swift - /// Item(classes: ["completed"], data: ["task-id": "123"]) { - /// "Complete documentation" - /// } - /// // Renders:
            4. Complete documentation
            5. - /// ``` - public init( - id: String? = nil, - classes: [String]? = nil, - role: AriaRole? = nil, - label: String? = nil, - data: [String: String]? = nil, - @HTMLBuilder content: @escaping () -> [any HTML] = { [] } - ) { - super.init( - tag: "li", - id: id, - classes: classes, - role: role, - label: label, - data: data, - content: content - ) - } -} diff --git a/Sources/WebUI/Elements/Base/Media.swift b/Sources/WebUI/Elements/Base/Media.swift deleted file mode 100644 index f7273d89..00000000 --- a/Sources/WebUI/Elements/Base/Media.swift +++ /dev/null @@ -1,499 +0,0 @@ -import Foundation - -/// Represents media size by width and height -/// -/// ## Example -/// ```swift -/// let size = MediaSize(width: 800, height: 600) -/// Image(source: "/images/hero.jpg", description: "Hero image", size: size) -/// ``` -public struct MediaSize { - let width: Int? - let height: Int? -} - -/// Enum for image MIME types -/// -/// ## Example -/// ```swift -/// Picture( -/// sources: [ -/// ("image.webp", .webp), -/// ("image.jpg", .jpeg) -/// ], -/// description: "Responsive image" -/// ) -/// ``` -public enum ImageType: String { - case jpeg = "image/jpeg" - case png = "image/png" - case webp = "image/webp" - case gif = "image/gif" -} - -/// Enum for video MIME types -/// -/// ## Example -/// ```swift -/// Video( -/// sources: [ -/// ("video.webm", .webm), -/// ("video.mp4", .mp4) -/// ], -/// controls: true -/// ) -/// ``` -public enum VideoType: String { - case mp4 = "video/mp4" - case webm = "video/webm" - case ogg = "video/ogg" -} - -/// Enum for audio MIME types -/// -/// ## Example -/// ```swift -/// Audio( -/// sources: [ -/// ("music.mp3", .mp3), -/// ("music.ogg", .ogg) -/// ], -/// controls: true, -/// loop: true -/// ) -/// ``` -public enum AudioType: String { - case mp3 = "audio/mpeg" - case ogg = "audio/ogg" - case wav = "audio/wav" -} - -/// Generates an HTML source element for media tags. -/// -/// ## Example -/// ```swift -/// Source(src: "video.mp4", type: "video/mp4") -/// ``` -public final class Source: Element { - let src: String - let type: String? - - /// Creates a new HTML source element. - /// - /// - Parameters: - /// - src: Source URL. - /// - type: MIME type, optional. - /// - data: Dictionary of `data-*` attributes for element relevant storing data. - /// - /// ## Example - /// ```swift - /// Source(src: "image.webp", type: "image/webp") - /// ``` - public init( - src: String, - type: String? = nil, - data: [String: String]? = nil - ) { - self.src = src - self.type = type - let customAttributes = [ - Attribute.string("src", src), - Attribute.string("type", type), - ].compactMap { $0 } - super.init( - tag: "source", - data: data, - isSelfClosing: true, - customAttributes: customAttributes.isEmpty ? nil : customAttributes - ) - } -} - -/// Generates an HTML img element. -/// -/// ## Example -/// ```swift -/// Image(source: "/images/logo.png", description: "Company Logo") -/// .rounded(.lg) -/// ``` -public final class Image: Element { - let source: String - let description: String - let size: MediaSize? - - /// Creates a new HTML img element. - /// - /// - Parameters: - /// - source: Where the image is located. - /// - description: Alt text for accessibility. - /// - size: Image size dimensions, optional. - /// - data: Dictionary of `data-*` attributes for element relevant storing data. - /// - /// ## Example - /// ```swift - /// Image( - /// source: "/images/profile.jpg", - /// description: "User Profile Photo", - /// size: MediaSize(width: 200, height: 200) - /// ) - /// .rounded(.full) - /// ``` - public init( - source: String, - description: String, - size: MediaSize? = nil, - data: [String: String]? = nil - ) { - self.source = source - self.description = description - self.size = size - let customAttributes = [ - Attribute.string("src", source), - Attribute.string("alt", description), - Attribute.string("width", size?.width?.description), - Attribute.string("height", size?.height?.description), - ].compactMap { $0 } - super.init( - tag: "img", - data: data, - isSelfClosing: true, - customAttributes: customAttributes.isEmpty ? nil : customAttributes - ) - } -} - -/// Generates an HTML picture element with multiple source tags. -/// Styles and attributes applied to this element are also passed to the nested Image element. -/// -/// ## Example -/// ```swift -/// Picture( -/// sources: [ -/// ("banner.webp", .webp), -/// ("banner.jpg", .jpeg) -/// ], -/// description: "Website banner image" -/// ) -/// ``` -public final class Picture: Element { - let sources: [(src: String, type: ImageType?)] - let description: String - let size: MediaSize? - - /// Creates a new HTML picture element. - /// - /// - Parameters: - /// - sources: Array of tuples containing source URL and optional image MIME type. - /// - description: Alt text for accessibility. - /// - size: Picture size dimensions, optional. - /// - id: Unique identifier for the HTML element. - /// - classes: An array of CSS classnames. - /// - role: ARIA role of the element for accessibility. - /// - label: ARIA label to describe the element. - /// - data: Dictionary of `data-*` attributes for element relevant storing data. - /// - /// All style attributes (id, classes, role, label, data) are passed to the nested Image element - /// to ensure proper styling, as the Picture element itself is invisible in the rendered output. - /// - /// ## Example - /// ```swift - /// Picture( - /// sources: [ - /// ("hero-large.webp", .webp), - /// ("hero-large.jpg", .jpeg) - /// ], - /// description: "Hero Banner", - /// id: "hero-image", - /// classes: ["responsive-image"] - /// ) - /// ``` - public init( - sources: [(src: String, type: ImageType?)], - description: String, - size: MediaSize? = nil, - id: String? = nil, - classes: [String]? = nil, - role: AriaRole? = nil, - label: String? = nil, - data: [String: String]? = nil - ) { - self.sources = sources - self.description = description - self.size = size - super.init( - tag: "picture", - id: id, - classes: classes, - role: role, - label: label, - data: data, - content: { - for source in sources { - Source(src: source.src, type: source.type?.rawValue) - } - Image( - source: sources[0].src, - description: description, - size: size, - data: data - ) - } - ) - } -} - -/// Generates an HTML figure element with a picture and figcaption. -/// Styles and attributes applied to this element are passed to the nested Picture element, -/// which further passes them to its nested Image element. -/// -/// ## Example -/// ```swift -/// Figure( -/// sources: [ -/// ("chart.webp", .webp), -/// ("chart.png", .png) -/// ], -/// description: "Annual revenue growth chart" -/// ) -/// ``` -public final class Figure: Element { - let sources: [(src: String, type: ImageType?)] - let description: String - let size: MediaSize? - - /// Creates a new HTML figure element containing a picture and figcaption. - /// - /// - Parameters: - /// - sources: Array of tuples containing source URL and optional image MIME type. - /// - description: Text for the figcaption and alt text for accessibility. - /// - size: Picture size dimensions, optional. - /// - id: Unique identifier for the HTML element. - /// - classes: An array of CSS classnames. - /// - role: ARIA role of the element for accessibility. - /// - label: ARIA label to describe the element. - /// - data: Dictionary of `data-*` attributes for element relevant storing data. - /// - /// All style attributes (id, classes, role, label, data) are passed to the nested Picture element - /// and ultimately to the Image element, ensuring proper styling throughout the hierarchy. - /// - /// ## Example - /// ```swift - /// Figure( - /// sources: [ - /// ("product.webp", .webp), - /// ("product.jpg", .jpeg) - /// ], - /// description: "Product XYZ with special features", - /// classes: ["product-figure", "bordered"] - /// ) - /// ``` - public init( - sources: [(src: String, type: ImageType?)], - description: String, - size: MediaSize? = nil, - id: String? = nil, - classes: [String]? = nil, - role: AriaRole? = nil, - label: String? = nil, - data: [String: String]? = nil - ) { - self.sources = sources - self.description = description - self.size = size - super.init( - tag: "figure", - id: id, - classes: classes, - role: role, - label: label, - data: data, - content: { - Picture( - sources: sources, - description: description, - size: size, - id: id, - classes: classes, - role: role, - label: label, - data: data - ) - Element( - tag: "figcaption", - content: { description } - ) - } - ) - } -} - -/// Generates an HTML video element with multiple source tags. -/// -/// ## Example -/// ```swift -/// Video( -/// sources: [ -/// ("intro.webm", .webm), -/// ("intro.mp4", .mp4) -/// ], -/// controls: true, -/// autoplay: false -/// ) -/// ``` -public final class Video: Element { - let sources: [(src: String, type: VideoType?)] - let controls: Bool? - let autoplay: Bool? - let loop: Bool? - let size: MediaSize? - - /// Creates a new HTML video element with embedded sources. - /// - /// - Parameters: - /// - sources: Array of tuples containing source URL and optional video MIME type. - /// - controls: Displays playback controls if true, optional. - /// - autoplay: Automatically starts playback if true, optional. - /// - loop: Repeats video playback if true, optional. - /// - size: Video size dimensions, optional. - /// - id: Unique identifier for the HTML element. - /// - classes: An array of CSS classnames. - /// - role: ARIA role of the element for accessibility. - /// - label: ARIA label to describe the element. - /// - data: Dictionary of `data-*` attributes for element relevant storing data. - /// - /// ## Example - /// ```swift - /// Video( - /// sources: [ - /// ("tutorial.webm", .webm), - /// ("tutorial.mp4", .mp4) - /// ], - /// controls: true, - /// loop: true, - /// size: MediaSize(width: 1280, height: 720), - /// id: "tutorial-video", - /// classes: ["responsive-video"] - /// ) - /// ``` - public init( - sources: [(src: String, type: VideoType?)], - controls: Bool? = nil, - autoplay: Bool? = nil, - loop: Bool? = nil, - size: MediaSize? = nil, - id: String? = nil, - classes: [String]? = nil, - role: AriaRole? = nil, - label: String? = nil, - data: [String: String]? = nil - ) { - self.sources = sources - self.controls = controls - self.autoplay = autoplay - self.loop = loop - self.size = size - let customAttributes = [ - Attribute.bool("controls", controls), - Attribute.bool("autoplay", autoplay), - Attribute.bool("loop", loop), - Attribute.string("width", size?.width?.description), - Attribute.string("height", size?.height?.description), - ].compactMap { $0 } - super.init( - tag: "video", - id: id, - classes: classes, - role: role, - label: label, - data: data, - customAttributes: customAttributes.isEmpty ? nil : customAttributes, - content: { - for source in sources { - Source(src: source.src, type: source.type?.rawValue) - } - "Your browser does not support the video tag." - } - ) - } -} - -/// Generates an HTML audio element with multiple source tags. -/// -/// ## Example -/// ```swift -/// Audio( -/// sources: [ -/// ("background.mp3", .mp3), -/// ("background.ogg", .ogg) -/// ], -/// controls: true -/// ) -/// ``` -public final class Audio: Element { - let sources: [(src: String, type: AudioType?)] - let controls: Bool? - let autoplay: Bool? - let loop: Bool? - - /// Creates a new HTML audio element with sources. - /// - /// - Parameters: - /// - sources: Array of tuples containing source URL and optional audio MIME type. - /// - controls: Displays playback controls if true, optional. - /// - autoplay: Automatically starts playback if true, optional. - /// - loop: Repeats audio playback if true, optional. - /// - id: Unique identifier for the HTML element. - /// - classes: An array of CSS classnames. - /// - role: ARIA role of the element for accessibility. - /// - label: ARIA label to describe the element. - /// - data: Dictionary of `data-*` attributes for element relevant storing data. - /// - /// ## Example - /// ```swift - /// Audio( - /// sources: [ - /// ("podcast.mp3", .mp3), - /// ("podcast.ogg", .ogg) - /// ], - /// controls: true, - /// id: "podcast-player", - /// label: "Episode 42: Web Development with Swift" - /// ) - /// ``` - public init( - sources: [(src: String, type: AudioType?)], - controls: Bool? = nil, - autoplay: Bool? = nil, - loop: Bool? = nil, - id: String? = nil, - classes: [String]? = nil, - role: AriaRole? = nil, - label: String? = nil, - data: [String: String]? = nil - ) { - self.sources = sources - self.controls = controls - self.autoplay = autoplay - self.loop = loop - let customAttributes = [ - Attribute.bool("controls", controls), - Attribute.bool("autoplay", autoplay), - Attribute.bool("loop", loop), - ].compactMap { $0 } - super.init( - tag: "audio", - id: id, - classes: classes, - role: role, - label: label, - data: data, - customAttributes: customAttributes.isEmpty ? nil : customAttributes, - content: { - for source in sources { - Source(src: source.src, type: source.type?.rawValue) - } - "Your browser does not support the audio element." - } - ) - } -} diff --git a/Sources/WebUI/Elements/Base/Text.swift b/Sources/WebUI/Elements/Base/Text.swift deleted file mode 100644 index 98bb0d72..00000000 --- a/Sources/WebUI/Elements/Base/Text.swift +++ /dev/null @@ -1,324 +0,0 @@ -import Foundation - -/// Generates HTML text elements as `

              ` or `` based on content. -/// -/// Paragraphs are for long form content with multiple sentences and -/// a `` tag is used for a single sentence of text and grouping inline content. -public final class Text: Element { - /// Creates a new text element. - /// - /// Uses `

              ` for multiple sentences, `` for one or fewer. - /// - /// - Parameters: - /// - id: Unique identifier for the HTML element. - /// - classes: An array of CSS classnames. - /// - role: ARIA role of the element for accessibility. - /// - label: ARIA label to describe the element. - /// - data: Dictionary of `data-*` attributes for element relevant storing data. - /// - content: Closure providing text content. - public init( - id: String? = nil, - classes: [String]? = nil, - role: AriaRole? = nil, - label: String? = nil, - data: [String: String]? = nil, - @HTMLBuilder content: @escaping () -> [any HTML] - ) { - let renderedContent = content().map { $0.render() }.joined() - let sentenceCount = renderedContent.components( - separatedBy: CharacterSet(charactersIn: ".!?") - ) - .filter { !$0.trimmingCharacters(in: .whitespaces).isEmpty } - .count - let tag = sentenceCount > 1 ? "p" : "span" - super.init( - tag: tag, - id: id, - classes: classes, - role: role, - label: label, - data: data, - content: content - ) - } -} - -/// Defines levels for HTML heading tags from h1 to h6. -public enum HeadingLevel: String { - /// Large title, most prominent heading (h1). - case largeTitle = "h1" - /// Title, second most prominent heading (h2). - case title = "h2" - /// Headline, third most prominent heading (h3). - case headline = "h3" - /// Subheadline, fourth most prominent heading (h4). - case subheadline = "h4" - /// Body, fifth most prominent heading (h5). - case body = "h5" - /// Footnote, least prominent heading (h6). - case footnote = "h6" -} - -/// Generates HTML heading elements from `

              ` to `

              `. -/// -/// The level of the heading should follow a semantic hierarchy through the document, -/// with `.title` for the main page title, `.section` for major sections, and -/// progressively more detailed levels (`.subsection`, `.topic`, etc.) for nested content. -public final class Heading: Element { - /// Creates a new heading. - /// - /// - Parameters: - /// - level: Heading level (.largeTitle, .title, .headline, .subheadline, .body, or .footnote). - /// - id: Unique identifier for the HTML element. - /// - classes: An array of CSS classnames. - /// - role: ARIA role of the element for accessibility. - /// - label: ARIA label to describe the element. - /// - data: Dictionary of `data-*` attributes for element relevant storing data. - /// - content: Closure providing heading content. - public init( - _ level: HeadingLevel, - id: String? = nil, - classes: [String]? = nil, - role: AriaRole? = nil, - label: String? = nil, - data: [String: String]? = nil, - @HTMLBuilder content: @escaping () -> [any HTML] = { [] } - ) { - super.init( - tag: level.rawValue, - id: id, - classes: classes, - role: role, - label: label, - data: data, - content: content - ) - } -} - -/// Generates an HTML anchor element; for linking to other locations. -public final class Link: Element { - private let href: String - private let newTab: Bool? - - /// Creates a new anchor link. - /// - /// - Parameters: - /// - destination: URL or path the link points to. - /// - newTab: Opens in a new tab if true, optional. - /// - id: Unique identifier for the HTML element. - /// - classes: An array of CSS classnames. - /// - role: ARIA role of the element for accessibility. - /// - label: ARIA label to describe the element. - /// - data: Dictionary of `data-*` attributes for element relevant storing data. - /// - content: Closure providing link content. - public init( - to destination: String, - newTab: Bool? = nil, - id: String? = nil, - classes: [String]? = nil, - role: AriaRole? = nil, - label: String? = nil, - data: [String: String]? = nil, - @HTMLBuilder content: @escaping () -> [any HTML] = { [] } - ) { - self.href = destination - self.newTab = newTab - - var attributes = [Attribute.string("href", destination)].compactMap { $0 } - - if newTab == true { - attributes.append(contentsOf: [ - "target=\"_blank\"", - "rel=\"noreferrer\"", - ]) - } - - super.init( - tag: "a", - id: id, - classes: classes, - role: role, - label: label, - data: data, - customAttributes: attributes.isEmpty ? nil : attributes, - content: content - ) - } -} - -/// Generates an HTML emphasis element. -/// -/// To be used to draw attention to text within another body of text. -public final class Emphasis: Element { - /// Creates a new emphasis element. - /// - /// - Parameters: - /// - id: Unique identifier for the HTML element. - /// - classes: An array of CSS classnames. - /// - role: ARIA role of the element for accessibility. - /// - label: ARIA label to describe the element. - /// - data: Dictionary of `data-*` attributes for element relevant storing data. - /// - content: Closure providing emphasized content. - public init( - id: String? = nil, - classes: [String]? = nil, - role: AriaRole? = nil, - label: String? = nil, - data: [String: String]? = nil, - @HTMLBuilder content: @escaping () -> [any HTML] = { [] } - ) { - super.init( - tag: "em", - id: id, - classes: classes, - role: role, - label: label, - data: data, - content: content - ) - } -} - -/// Generates an HTML strong importance element. -/// -/// To be used for drawing strong attention to text within another body of text. -public final class Strong: Element { - /// Creates a new strong element. - /// - /// - Parameters: - /// - id: Unique identifier for the HTML element. - /// - classes: An array of CSS classnames. - /// - role: ARIA role of the element for accessibility. - /// - label: ARIA label to describe the element. - /// - data: Dictionary of `data-*` attributes for element relevant storing data. - /// - content: Closure providing strong content. - public init( - id: String? = nil, - classes: [String]? = nil, - role: AriaRole? = nil, - label: String? = nil, - data: [String: String]? = nil, - @HTMLBuilder content: @escaping () -> [any HTML] = { [] } - ) { - super.init( - tag: "strong", - id: id, - classes: classes, - role: role, - label: label, - data: data, - content: content - ) - } -} - -/// Generates an HTML time element. -/// -/// Used to represent a specific date, time, or duration in a machine-readable format. -/// The datetime attribute provides the machine-readable value while the content -/// can be a human-friendly representation. -public final class Time: Element { - private let datetime: String - - /// Creates a new time element. - /// - /// - Parameters: - /// - datetime: Machine-readable date/time in ISO 8601 format (e.g., "2025-03-22" or "2025-03-22T14:30:00Z"). - /// - id: Unique identifier for the HTML element. - /// - classes: An array of CSS classnames. - /// - role: ARIA role of the element for accessibility. - /// - label: ARIA label to describe the element. - /// - data: Dictionary of `data-*` attributes for element relevant storing data. - /// - content: Closure providing human-readable time content. - public init( - datetime: String, - id: String? = nil, - classes: [String]? = nil, - role: AriaRole? = nil, - label: String? = nil, - data: [String: String]? = nil, - @HTMLBuilder content: @escaping () -> [any HTML] = { [] } - ) { - self.datetime = datetime - let customAttributes = [ - Attribute.string("datetime", datetime) - ].compactMap { $0 } - super.init( - tag: "time", - id: id, - classes: classes, - role: role, - label: label, - data: data, - customAttributes: customAttributes.isEmpty ? nil : customAttributes, - content: content - ) - } -} - -/// Generates an HTML code block element -/// -/// To be used for rendering code examples on a web page -public final class Code: Element { - /// Creates a new code element. - /// - /// - Parameters: - /// - id: Unique identifier for the HTML element. - /// - classes: An array of CSS classnames. - /// - role: ARIA role of the element for accessibility. - /// - label: ARIA label to describe the element. - /// - data: Dictionary of `data-*` attributes for element relevant storing data. - /// - content: Closure providing code content. - public init( - id: String? = nil, - classes: [String]? = nil, - role: AriaRole? = nil, - label: String? = nil, - data: [String: String]? = nil, - @HTMLBuilder content: @escaping () -> [any HTML] = { [] } - ) { - super.init( - tag: "code", - id: id, - classes: classes, - role: role, - label: label, - data: data, - content: content - ) - } -} - -/// Generates an HTML pre element -/// -/// To be used for rendering preformatted text such as groups of code elements. -public final class Preformatted: Element { - /// Creates a new preformatted element. - /// - /// - Parameters: - /// - id: Unique identifier for the HTML element. - /// - classes: An array of CSS classnames. - /// - role: ARIA role of the element for accessibility. - /// - label: ARIA label to describe the element. - /// - data: Dictionary of `data-*` attributes for element relevant storing data. - /// - content: Closure providing preformatted content. - public init( - id: String? = nil, - classes: [String]? = nil, - role: AriaRole? = nil, - label: String? = nil, - data: [String: String]? = nil, - @HTMLBuilder content: @escaping () -> [any HTML] = { [] } - ) { - super.init( - tag: "pre", - id: id, - classes: classes, - role: role, - label: label, - data: data, - content: content - ) - } -} diff --git a/Sources/WebUI/Elements/Base/Button.swift b/Sources/WebUI/Elements/Interactive/Button.swift similarity index 100% rename from Sources/WebUI/Elements/Base/Button.swift rename to Sources/WebUI/Elements/Interactive/Button.swift diff --git a/Sources/WebUI/Elements/Form/Form.swift b/Sources/WebUI/Elements/Interactive/Form/Form.swift similarity index 100% rename from Sources/WebUI/Elements/Form/Form.swift rename to Sources/WebUI/Elements/Interactive/Form/Form.swift diff --git a/Sources/WebUI/Elements/Form/Input.swift b/Sources/WebUI/Elements/Interactive/Form/Input/Input.swift similarity index 84% rename from Sources/WebUI/Elements/Form/Input.swift rename to Sources/WebUI/Elements/Interactive/Form/Input/Input.swift index ea7421d3..2de3484e 100644 --- a/Sources/WebUI/Elements/Form/Input.swift +++ b/Sources/WebUI/Elements/Interactive/Form/Input/Input.swift @@ -1,22 +1,3 @@ -/// Defines types for HTML input elements. -/// -/// Specifies the type of data to be collected by an input element, affecting both -/// appearance and validation behavior. -public enum InputType: String { - /// Single-line text input field for general text entry. - case text - /// Masked password input field that hides characters for security. - case password - /// Email address input field with validation for email format. - case email - /// Numeric input field optimized for number entry, often with increment/decrement controls. - case number - /// Checkbox input for boolean (yes/no) selections. - case checkbox - /// Submit button input that triggers form submission when clicked. - case submit -} - /// Generates an HTML input element for collecting user input, such as text or numbers. /// /// `Input` elements are the primary way to gather user data in forms, supporting various types diff --git a/Sources/WebUI/Elements/Interactive/Form/Input/InputType.swift b/Sources/WebUI/Elements/Interactive/Form/Input/InputType.swift new file mode 100644 index 00000000..dcb1db87 --- /dev/null +++ b/Sources/WebUI/Elements/Interactive/Form/Input/InputType.swift @@ -0,0 +1,18 @@ +/// Defines types for HTML input elements. +/// +/// Specifies the type of data to be collected by an input element, affecting both +/// appearance and validation behavior. +public enum InputType: String { + /// Single-line text input field for general text entry. + case text + /// Masked password input field that hides characters for security. + case password + /// Email address input field with validation for email format. + case email + /// Numeric input field optimized for number entry, often with increment/decrement controls. + case number + /// Checkbox input for boolean (yes/no) selections. + case checkbox + /// Submit button input that triggers form submission when clicked. + case submit +} diff --git a/Sources/WebUI/Elements/Form/Label.swift b/Sources/WebUI/Elements/Interactive/Form/Input/Label.swift similarity index 100% rename from Sources/WebUI/Elements/Form/Label.swift rename to Sources/WebUI/Elements/Interactive/Form/Input/Label.swift diff --git a/Sources/WebUI/Elements/Form/TextArea.swift b/Sources/WebUI/Elements/Interactive/Form/TextArea.swift similarity index 100% rename from Sources/WebUI/Elements/Form/TextArea.swift rename to Sources/WebUI/Elements/Interactive/Form/TextArea.swift diff --git a/Sources/WebUI/Elements/Form/Progress.swift b/Sources/WebUI/Elements/Interactive/Progress.swift similarity index 100% rename from Sources/WebUI/Elements/Form/Progress.swift rename to Sources/WebUI/Elements/Interactive/Progress.swift diff --git a/Sources/WebUI/Elements/Layout/Layout.swift b/Sources/WebUI/Elements/Layout/Layout.swift deleted file mode 100644 index 65fb28d2..00000000 --- a/Sources/WebUI/Elements/Layout/Layout.swift +++ /dev/null @@ -1,273 +0,0 @@ -/// Generates an HTML header element for page or section headers. -/// -/// The `Header` element represents a container for introductory content or a set of navigational links -/// at the beginning of a section or page. Typically contains elements like site logos, navigation menus, -/// and search forms. -/// -/// ## Example -/// ```swift -/// Header { -/// Heading(.largeTitle) { "Site Title" } -/// Navigation { -/// Link(to: "/home") { "Home" } -/// Link(to: "/about") { "About" } -/// } -/// } -/// ``` -public final class Header: Element { - /// Creates a new HTML header element. - /// - /// - Parameters: - /// - id: Unique identifier for the HTML element, useful for styling and scripting. - /// - classes: An array of CSS classnames for styling the header. - /// - role: ARIA role of the element for accessibility and screen readers. - /// - label: ARIA label to describe the element for screen readers. - /// - data: Dictionary of `data-*` attributes for storing custom data related to the header. - /// - content: Closure providing header content like headings, navigation, and logos. - /// - /// ## Example - /// ```swift - /// Header(id: "main-header", classes: ["site-header", "sticky"]) { - /// Heading(.largeTitle) { "My Website" } - /// } - /// ``` - public init( - id: String? = nil, - classes: [String]? = nil, - role: AriaRole? = nil, - label: String? = nil, - data: [String: String]? = nil, - @HTMLBuilder content: @escaping () -> [any HTML] = { [] } - ) { - super.init( - tag: "header", - id: id, - classes: classes, - role: role, - label: label, - data: data, - content: content - ) - } -} - -/// Generates an HTML navigation element for site navigation. -/// -/// The `Navigation` element represents a section of a page intended to contain navigation -/// links to other pages or parts within the current page. It helps screen readers and -/// other assistive technologies identify the main navigation structure of the website. -/// -/// ## Example -/// ```swift -/// Navigation(classes: ["main-nav"]) { -/// List { -/// Item { Link(to: "/") { "Home" } } -/// Item { Link(to: "/products") { "Products" } } -/// Item { Link(to: "/contact") { "Contact" } } -/// } -/// } -/// ``` -public final class Navigation: Element { - /// Creates a new HTML navigation element. - /// - /// - Parameters: - /// - id: Unique identifier for the HTML element, useful for styling and scripting. - /// - classes: An array of CSS classnames for styling the navigation container. - /// - role: ARIA role of the element for accessibility and screen readers. - /// - label: ARIA label to describe the element's purpose (e.g., "Main Navigation"). - /// - data: Dictionary of `data-*` attributes for storing custom data related to navigation. - /// - content: Closure providing navigation content, typically links or lists of links. - /// - /// ## Example - /// ```swift - /// Navigation(id: "main-nav", label: "Main Navigation") { - /// Link(to: "/home") { "Home" } - /// Link(to: "/about") { "About Us" } - /// } - /// ``` - public init( - id: String? = nil, - classes: [String]? = nil, - role: AriaRole? = nil, - label: String? = nil, - data: [String: String]? = nil, - @HTMLBuilder content: @escaping () -> [any HTML] = { [] } - ) { - super.init( - tag: "nav", - id: id, - classes: classes, - role: role, - label: label, - data: data, - content: content - ) - } -} - -/// Generates an HTML aside element for tangentially related content. -/// -/// The `Aside` element represents a section of content that is indirectly related to the -/// main content but could be considered separate. Asides are typically displayed as -/// sidebars or call-out boxes, containing content like related articles, glossary terms, -/// advertisements, or author biographies. -/// -/// ## Example -/// ```swift -/// Aside(classes: ["sidebar"]) { -/// Heading(.title) { "Related Articles" } -/// List { -/// Item { Link(to: "/article1") { "Article 1" } } -/// Item { Link(to: "/article2") { "Article 2" } } -/// } -/// } -/// ``` -public final class Aside: Element { - /// Creates a new HTML aside element. - /// - /// - Parameters: - /// - id: Unique identifier for the HTML element, useful for styling and scripting. - /// - classes: An array of CSS classnames for styling the aside container. - /// - role: ARIA role of the element for accessibility and screen readers. - /// - label: ARIA label to describe the element's purpose (e.g., "Related Content"). - /// - data: Dictionary of `data-*` attributes for storing custom data related to the aside. - /// - content: Closure providing aside content, such as related links, footnotes, or supplementary information. - /// - /// ## Example - /// ```swift - /// Aside(id: "glossary", classes: ["note", "bordered"], label: "Term Definition") { - /// Heading(.headline) { "Definition" } - /// Text { "A detailed explanation of the term..." } - /// } - /// ``` - public init( - id: String? = nil, - classes: [String]? = nil, - role: AriaRole? = nil, - label: String? = nil, - data: [String: String]? = nil, - @HTMLBuilder content: @escaping () -> [any HTML] = { [] } - ) { - super.init( - tag: "aside", - id: id, - classes: classes, - role: role, - label: label, - data: data, - content: content - ) - } -} - -/// Generates an HTML main element for the primary content of a page. -/// -/// The `Main` element represents the dominant content of the document body. It contains content -/// that is directly related to or expands upon the central topic of the document. Each page -/// should have only one `main` element, which helps assistive technologies navigate to the -/// primary content. -/// -/// ## Example -/// ```swift -/// Main { -/// Heading(.largeTitle) { "Welcome to Our Website" } -/// Text { "This is the main content of our homepage." } -/// Article { -/// // Article content -/// } -/// } -/// ``` -public final class Main: Element { - /// Creates a new HTML main element. - /// - /// - Parameters: - /// - id: Unique identifier for the HTML element, useful for styling and scripting. - /// - classes: An array of CSS classnames for styling the main content area. - /// - role: ARIA role of the element for accessibility and screen readers. - /// - label: ARIA label to describe the element's purpose (e.g., "Main Content"). - /// - data: Dictionary of `data-*` attributes for storing custom data related to the main content. - /// - content: Closure providing the primary content of the page, typically including articles, sections, and other content elements. - /// - /// ## Example - /// ```swift - /// Main(id: "content", classes: ["container"]) { - /// Section { - /// Heading(.largeTitle) { "About Us" } - /// Text { "Learn more about our company history..." } - /// } - /// } - /// ``` - public init( - id: String? = nil, - classes: [String]? = nil, - role: AriaRole? = nil, - label: String? = nil, - data: [String: String]? = nil, - @HTMLBuilder content: @escaping () -> [any HTML] = { [] } - ) { - super.init( - tag: "main", - id: id, - classes: classes, - role: role, - label: label, - data: data, - content: content - ) - } -} - -/// Generates an HTML footer element for page or section footers. -/// -/// The `Footer` element represents a footer for its nearest sectioning content or sectioning root -/// element. A footer typically contains information about the author, copyright data, related links, -/// legal information, and other metadata that appears at the end of a document or section. -/// -/// ## Example -/// ```swift -/// Footer { -/// Text { "© 2023 My Company. All rights reserved." } -/// Link(to: "/privacy") { "Privacy Policy" } -/// Link(to: "/terms") { "Terms of Service" } -/// } -/// ``` -public final class Footer: Element { - /// Creates a new HTML footer element. - /// - /// - Parameters: - /// - id: Unique identifier for the HTML element, useful for styling and scripting. - /// - classes: An array of CSS classnames for styling the footer. - /// - role: ARIA role of the element for accessibility and screen readers. - /// - label: ARIA label to describe the element's purpose (e.g., "Page Footer"). - /// - data: Dictionary of `data-*` attributes for storing custom data related to the footer. - /// - content: Closure providing footer content, such as copyright notices, contact information, and secondary navigation. - /// - /// ## Example - /// ```swift - /// Footer(id: "site-footer", classes: ["footer", "bg-dark"]) { - /// Stack(classes: ["footer-links"]) { - /// Link(to: "/about") { "About" } - /// Link(to: "/contact") { "Contact" } - /// } - /// Text { "© \(Date().formattedYear()) My Company" } - /// } - /// ``` - public init( - id: String? = nil, - classes: [String]? = nil, - role: AriaRole? = nil, - label: String? = nil, - data: [String: String]? = nil, - @HTMLBuilder content: @escaping () -> [any HTML] = { [] } - ) { - super.init( - tag: "footer", - id: id, - classes: classes, - role: role, - label: label, - data: data, - content: content - ) - } -} diff --git a/Sources/WebUI/Elements/Layout/Structure.swift b/Sources/WebUI/Elements/Layout/Structure.swift deleted file mode 100644 index 073d9069..00000000 --- a/Sources/WebUI/Elements/Layout/Structure.swift +++ /dev/null @@ -1,162 +0,0 @@ -/// Generates an HTML article element for self-contained content sections. -/// -/// Represents a self-contained, independently distributable composition like a blog post, -/// news story, forum post, or any content that could stand alone. Articles are ideal for -/// content that could be syndicated or reused elsewhere. -/// -/// ## Example -/// ```swift -/// Article { -/// Heading(.largeTitle) { "Blog Post Title" } -/// Text { "Published on May 15, 2023" } -/// Text { "This is the content of the blog post..." } -/// } -/// ``` -public final class Article: Element { - /// Creates a new HTML article element for self-contained content. - /// - /// - Parameters: - /// - id: Unique identifier for the HTML element, useful for linking and scripting. - /// - classes: An array of CSS classnames for styling the article. - /// - role: ARIA role of the element for accessibility and screen readers. - /// - label: ARIA label to describe the element for screen readers. - /// - data: Dictionary of `data-*` attributes for storing custom data related to the article. - /// - content: Closure providing article content such as headings, paragraphs, and media. - /// - /// ## Example - /// ```swift - /// Article(id: "post-123", classes: ["blog-post", "featured"]) { - /// Heading(.largeTitle) { "Getting Started with WebUI" } - /// Text { "Learn how to build static websites using Swift..." } - /// } - /// ``` - public init( - id: String? = nil, - classes: [String]? = nil, - role: AriaRole? = nil, - label: String? = nil, - data: [String: String]? = nil, - @HTMLBuilder content: @escaping () -> [any HTML] = { [] } - ) { - super.init( - tag: "article", - id: id, - classes: classes, - role: role, - label: label, - data: data, - content: content - ) - } -} - -/// Generates an HTML section element for thematic content grouping. -/// -/// Defines a thematic grouping of content, such as a chapter, tab panel, or any content -/// that forms a distinct section of a document. Sections typically have their own heading -/// and represent a logical grouping of related content. -/// -/// ## Example -/// ```swift -/// Section(id: "features") { -/// Heading(.title) { "Key Features" } -/// List { -/// Item { "Simple API" } -/// Item { "Type-safe HTML generation" } -/// Item { "Responsive design" } -/// } -/// } -/// ``` -public final class Section: Element { - /// Creates a new HTML section element for thematic content grouping. - /// - /// - Parameters: - /// - id: Unique identifier for the HTML element, useful for navigation and linking. - /// - classes: An array of CSS classnames for styling the section. - /// - role: ARIA role of the element for accessibility and screen readers. - /// - label: ARIA label to describe the element's purpose for screen readers. - /// - data: Dictionary of `data-*` attributes for storing custom data related to the section. - /// - content: Closure providing section content such as headings, paragraphs, and other elements. - /// - /// ## Example - /// ```swift - /// Section(id: "about", classes: ["content-section"]) { - /// Heading(.title) { "About Us" } - /// Text { "Our company was founded in 2020..." } - /// } - /// ``` - public init( - id: String? = nil, - classes: [String]? = nil, - role: AriaRole? = nil, - label: String? = nil, - data: [String: String]? = nil, - @HTMLBuilder content: @escaping () -> [any HTML] = { [] } - ) { - super.init( - tag: "section", - id: id, - classes: classes, - role: role, - label: label, - data: data, - content: content - ) - } -} - -/// Generates an HTML div element for generic content grouping. -/// -/// The `Stack` element (which renders as a div) groups elements for styling or layout -/// without conveying any specific semantic meaning. It's a versatile container used -/// for creating layout structures, applying styles to groups, or providing hooks for -/// JavaScript functionality. -/// -/// - Note: Use semantic elements like `Article`, `Section`, or `Aside` when possible, -/// and reserve `Stack` for purely presentational grouping. -/// -/// ## Example -/// ```swift -/// Stack(classes: ["flex-container"]) { -/// Stack(classes: ["card"]) { "Card 1 content" } -/// Stack(classes: ["card"]) { "Card 2 content" } -/// } -/// ``` -public final class Stack: Element { - /// Creates a new HTML div element for generic content grouping. - /// - /// - Parameters: - /// - id: Unique identifier for the HTML element, useful for styling and scripting. - /// - classes: An array of CSS classnames for styling the div container. - /// - role: ARIA role of the element for accessibility and screen readers. - /// - label: ARIA label to describe the element's purpose for screen readers. - /// - data: Dictionary of `data-*` attributes for storing custom data related to the container. - /// - content: Closure providing the container's content elements. - /// - /// ## Example - /// ```swift - /// Stack(id: "user-profile", classes: ["card", "shadow"], data: ["user-id": "123"]) { - /// Image(source: "/avatar.jpg", description: "User Avatar") - /// Heading(.headline) { "Jane Doe" } - /// Text { "Software Engineer" } - /// } - /// ``` - public init( - id: String? = nil, - classes: [String]? = nil, - role: AriaRole? = nil, - label: String? = nil, - data: [String: String]? = nil, - @HTMLBuilder content: @escaping () -> [any HTML] = { [] } - ) { - super.init( - tag: "div", - id: id, - classes: classes, - role: role, - label: label, - data: data, - content: content - ) - } -} diff --git a/Sources/WebUI/Elements/Media/Audio/Audio.swift b/Sources/WebUI/Elements/Media/Audio/Audio.swift new file mode 100644 index 00000000..3567463e --- /dev/null +++ b/Sources/WebUI/Elements/Media/Audio/Audio.swift @@ -0,0 +1,83 @@ +/// Creates HTML audio elements for playing sound content. +/// +/// Represents an audio player that supports multiple source formats for cross-browser compatibility. +/// Audio elements are useful for embedding sound content such as music, podcasts, or sound effects. +/// +/// ## Example +/// ```swift +/// Audio( +/// sources: [ +/// ("background.mp3", .mp3), +/// ("background.ogg", .ogg) +/// ], +/// controls: true +/// ) +/// ``` +public final class Audio: Element { + let sources: [(src: String, type: AudioType?)] + let controls: Bool? + let autoplay: Bool? + let loop: Bool? + + /// Creates a new HTML audio player. + /// + /// - Parameters: + /// - sources: Array of tuples containing source URL and optional audio MIME type. + /// - controls: Displays playback controls if true, optional. + /// - autoplay: Automatically starts playback if true, optional. + /// - loop: Repeats audio playback if true, optional. + /// - id: Unique identifier for the HTML element, useful for JavaScript interaction and styling. + /// - classes: An array of CSS classnames for styling the audio player. + /// - role: ARIA role of the element for accessibility, enhancing screen reader interpretation. + /// - label: ARIA label to describe the element for accessibility when context isn't sufficient. + /// - data: Dictionary of `data-*` attributes for storing custom data relevant to the audio player. + /// + /// ## Example + /// ```swift + /// Audio( + /// sources: [ + /// ("podcast.mp3", .mp3), + /// ("podcast.ogg", .ogg) + /// ], + /// controls: true, + /// id: "podcast-player", + /// label: "Episode 42: Web Development with Swift" + /// ) + /// ``` + public init( + sources: [(src: String, type: AudioType?)], + controls: Bool? = nil, + autoplay: Bool? = nil, + loop: Bool? = nil, + id: String? = nil, + classes: [String]? = nil, + role: AriaRole? = nil, + label: String? = nil, + data: [String: String]? = nil + ) { + self.sources = sources + self.controls = controls + self.autoplay = autoplay + self.loop = loop + let customAttributes = [ + Attribute.bool("controls", controls), + Attribute.bool("autoplay", autoplay), + Attribute.bool("loop", loop), + ].compactMap { $0 } + super.init( + tag: "audio", + id: id, + classes: classes, + role: role, + label: label, + data: data, + customAttributes: customAttributes.isEmpty ? nil : customAttributes, + content: { + for source in sources { + Source(src: source.src, type: source.type?.rawValue) + } + "Your browser does not support the audio element." + } + ) + } +} diff --git a/Sources/WebUI/Elements/Media/Audio/AudioType.swift b/Sources/WebUI/Elements/Media/Audio/AudioType.swift new file mode 100644 index 00000000..f2e0d359 --- /dev/null +++ b/Sources/WebUI/Elements/Media/Audio/AudioType.swift @@ -0,0 +1,21 @@ +/// Defines audio MIME types for use with audio elements. +/// +/// Used to specify the format of audio files, ensuring browsers can properly interpret and play the content. +/// Different browsers support different audio formats, so providing multiple source types improves compatibility. +/// +/// ## Example +/// ```swift +/// Audio( +/// sources: [ +/// ("music.mp3", .mp3), +/// ("music.ogg", .ogg) +/// ], +/// controls: true, +/// loop: true +/// ) +/// ``` +public enum AudioType: String { + case mp3 = "audio/mpeg" + case ogg = "audio/ogg" + case wav = "audio/wav" +} diff --git a/Sources/WebUI/Elements/Media/Image/Figure.swift b/Sources/WebUI/Elements/Media/Image/Figure.swift new file mode 100644 index 00000000..179ee520 --- /dev/null +++ b/Sources/WebUI/Elements/Media/Image/Figure.swift @@ -0,0 +1,84 @@ +/// Generates an HTML figure element with a picture and figcaption. +/// Styles and attributes applied to this element are passed to the nested Picture element, +/// which further passes them to its nested Image element. +/// +/// ## Example +/// ```swift +/// Figure( +/// sources: [ +/// ("chart.webp", .webp), +/// ("chart.png", .png) +/// ], +/// description: "Annual revenue growth chart" +/// ) +/// ``` +public final class Figure: Element { + let sources: [(src: String, type: ImageType?)] + let description: String + let size: MediaSize? + + /// Creates a new HTML figure element containing a picture and figcaption. + /// + /// - Parameters: + /// - sources: Array of tuples containing source URL and optional image MIME type. + /// - description: Text for the figcaption and alt text for accessibility. + /// - size: Picture size dimensions, optional. + /// - id: Unique identifier for the HTML element. + /// - classes: An array of CSS classnames. + /// - role: ARIA role of the element for accessibility. + /// - label: ARIA label to describe the element. + /// - data: Dictionary of `data-*` attributes for element relevant storing data. + /// + /// All style attributes (id, classes, role, label, data) are passed to the nested Picture element + /// and ultimately to the Image element, ensuring proper styling throughout the hierarchy. + /// + /// ## Example + /// ```swift + /// Figure( + /// sources: [ + /// ("product.webp", .webp), + /// ("product.jpg", .jpeg) + /// ], + /// description: "Product XYZ with special features", + /// classes: ["product-figure", "bordered"] + /// ) + /// ``` + public init( + sources: [(src: String, type: ImageType?)], + description: String, + size: MediaSize? = nil, + id: String? = nil, + classes: [String]? = nil, + role: AriaRole? = nil, + label: String? = nil, + data: [String: String]? = nil + ) { + self.sources = sources + self.description = description + self.size = size + super.init( + tag: "figure", + id: id, + classes: classes, + role: role, + label: label, + data: data, + content: { + Picture( + sources: sources, + description: description, + size: size, + id: id, + classes: classes, + role: role, + label: label, + data: data + ) + Element( + tag: "figcaption", + content: { description } + ) + } + ) + } +} diff --git a/Sources/WebUI/Elements/Media/Image/Image.swift b/Sources/WebUI/Elements/Media/Image/Image.swift new file mode 100644 index 00000000..5497a67c --- /dev/null +++ b/Sources/WebUI/Elements/Media/Image/Image.swift @@ -0,0 +1,58 @@ +import Foundation + +/// Creates HTML image elements for displaying graphical content. +/// +/// Represents an image that can be embedded in a webpage, with support for accessibility +/// descriptions and sizing information. Images are fundamental for illustrating content +/// and enhancing visual communication. +/// +/// ## Example +/// ```swift +/// Image( +/// source: "logo.png", +/// description: "Company Logo", +/// type: .png, +/// size: MediaSize(width: 100, height: 100) +/// ) +/// ``` +public final class Image: Element { + /// Creates a new HTML image element. + /// + /// - Parameters: + /// - source: The image source URL or path. + /// - description: The alt text for the image for accessibility and SEO. + /// - type: The MIME type of the image, optional. + /// - size: The size of the image in pixels, optional. + /// - id: Unique identifier for the HTML element, useful for JavaScript interaction and styling. + /// - classes: An array of CSS classnames for styling the image. + /// - role: ARIA role of the element for accessibility, enhancing screen reader interpretation. + /// - label: ARIA label to describe the element for accessibility when alt text isn't sufficient. + /// - data: Dictionary of `data-*` attributes for storing custom data relevant to the image. + public init( + source: String, + description: String, + type: ImageType? = nil, + size: MediaSize? = nil, + id: String? = nil, + classes: [String]? = nil, + role: AriaRole? = nil, + label: String? = nil, + data: [String: String]? = nil + ) { + var attributes: [String] = ["src=\"\(source)\"", "alt=\"\(description)\""] + if let type = type { attributes.append("type=\"\(type.rawValue)\"") } + if let size = size { + if let width = size.width { attributes.append("width=\"\(width)\"") } + if let height = size.height { attributes.append("height=\"\(height)\"") } + } + super.init( + tag: "img", + id: id, + classes: classes, + role: role, + label: label, + data: data, + customAttributes: attributes + ) + } +} diff --git a/Sources/WebUI/Elements/Media/Image/ImageType.swift b/Sources/WebUI/Elements/Media/Image/ImageType.swift new file mode 100644 index 00000000..6cee7c3b --- /dev/null +++ b/Sources/WebUI/Elements/Media/Image/ImageType.swift @@ -0,0 +1,21 @@ +import Foundation + +/// Defines image MIME types for use with image elements. +/// +/// Used to specify the format of image files, ensuring browsers can properly interpret and display the content. +/// Different browsers support different image formats, so providing multiple source types improves compatibility. +/// +/// ## Example +/// ```swift +/// Image( +/// source: "photo.jpg", +/// description: "A beautiful landscape", +/// type: .jpeg +/// ) +/// ``` +public enum ImageType: String { + case jpeg = "image/jpeg" + case png = "image/png" + case webp = "image/webp" + case gif = "image/gif" +} diff --git a/Sources/WebUI/Elements/Media/Image/Picture.swift b/Sources/WebUI/Elements/Media/Image/Picture.swift new file mode 100644 index 00000000..45ecc250 --- /dev/null +++ b/Sources/WebUI/Elements/Media/Image/Picture.swift @@ -0,0 +1,79 @@ +/// Generates an HTML picture element with multiple source tags. +/// Styles and attributes applied to this element are also passed to the nested Image element. +/// +/// ## Example +/// ```swift +/// Picture( +/// sources: [ +/// ("banner.webp", .webp), +/// ("banner.jpg", .jpeg) +/// ], +/// description: "Website banner image" +/// ) +/// ``` +public final class Picture: Element { + let sources: [(src: String, type: ImageType?)] + let description: String + let size: MediaSize? + + /// Creates a new HTML picture element. + /// + /// - Parameters: + /// - sources: Array of tuples containing source URL and optional image MIME type. + /// - description: Alt text for accessibility. + /// - size: Picture size dimensions, optional. + /// - id: Unique identifier for the HTML element. + /// - classes: An array of CSS classnames. + /// - role: ARIA role of the element for accessibility. + /// - label: ARIA label to describe the element. + /// - data: Dictionary of `data-*` attributes for element relevant storing data. + /// + /// All style attributes (id, classes, role, label, data) are passed to the nested Image element + /// to ensure proper styling, as the Picture element itself is invisible in the rendered output. + /// + /// ## Example + /// ```swift + /// Picture( + /// sources: [ + /// ("hero-large.webp", .webp), + /// ("hero-large.jpg", .jpeg) + /// ], + /// description: "Hero Banner", + /// id: "hero-image", + /// classes: ["responsive-image"] + /// ) + /// ``` + public init( + sources: [(src: String, type: ImageType?)], + description: String, + size: MediaSize? = nil, + id: String? = nil, + classes: [String]? = nil, + role: AriaRole? = nil, + label: String? = nil, + data: [String: String]? = nil + ) { + self.sources = sources + self.description = description + self.size = size + super.init( + tag: "picture", + id: id, + classes: classes, + role: role, + label: label, + data: data, + content: { + for source in sources { + Source(src: source.src, type: source.type?.rawValue) + } + Image( + source: sources[0].src, + description: description, + size: size, + data: data + ) + } + ) + } +} diff --git a/Sources/WebUI/Elements/Media/MediaSize.swift b/Sources/WebUI/Elements/Media/MediaSize.swift new file mode 100644 index 00000000..4d823049 --- /dev/null +++ b/Sources/WebUI/Elements/Media/MediaSize.swift @@ -0,0 +1,29 @@ +import Foundation + +/// Specifies dimensions for media elements such as images, videos, and audio visualizations. +/// +/// This struct provides a consistent way to define width and height measurements for media elements, +/// helping ensure proper rendering and layout in HTML. Both dimensions are optional to accommodate +/// scenarios where only one dimension needs to be specified while maintaining aspect ratio. +/// +/// ## Example +/// ```swift +/// let size = MediaSize(width: 800, height: 600) +/// Video(sources: [("movie.mp4", .mp4)], size: size) +/// ``` +public struct MediaSize { + /// The width of the media in pixels. + public let width: Int? + /// The height of the media in pixels. + public let height: Int? + + /// Creates a new media size specification. + /// + /// - Parameters: + /// - width: Width dimension in pixels, optional. + /// - height: Height dimension in pixels, optional. + public init(width: Int? = nil, height: Int? = nil) { + self.width = width + self.height = height + } +} diff --git a/Sources/WebUI/Elements/Media/Source.swift b/Sources/WebUI/Elements/Media/Source.swift new file mode 100644 index 00000000..1388a679 --- /dev/null +++ b/Sources/WebUI/Elements/Media/Source.swift @@ -0,0 +1,64 @@ +import Foundation + +/// Creates HTML source elements for multimedia content. +/// +/// Specifies multiple media resources for the `