From a03bc60fe306a7625d6ab1f8def337263cb87bcc Mon Sep 17 00:00:00 2001 From: Mac Date: Fri, 23 May 2025 00:19:54 +0100 Subject: [PATCH 1/5] Documentation updated to match newer patterns --- Sources/WebUI/Core/Metadata/Metadata.swift | 2 + .../WebUI/Documentation.docc/CoreConcepts.md | 139 +++++ .../Documentation.docc/CustomElements.md | 199 +++++++ .../WebUI/Documentation.docc/FormElements.md | 189 +++++++ .../Documentation.docc/GettingStarted.md | 134 +++++ .../Documentation.docc/ResponsiveDesign.md | 175 ++++++ Sources/WebUI/Documentation.docc/SEOGuide.md | 179 +++++++ .../WebUI/Documentation.docc/StylingGuide.md | 176 ++++++ Sources/WebUI/Documentation.docc/WebUI.md | 116 ++-- .../Documentation.docc/getting-started.md | 123 ----- Sources/WebUI/Documentation.docc/styles.md | 107 ---- .../table-of-contents.tutorial | 13 - .../Display/FlexStyleOperation.swift | 68 +-- .../Display/GridStyleOperation.swift | 44 +- .../Display/VisibilityStyleOperation.swift | 18 +- .../Appearance/OutlineStyleOperation.swift | 36 +- .../Appearance/RingStyleOperation.swift | 4 +- .../Base/Sizing/SizingStyleOperation.swift | 2 +- Sources/WebUI/Styles/Base/Utilities.swift | 24 +- .../Styles/Core/InteractionModifiers.swift | 36 +- .../WebUI/Styles/Core/ResponsiveAlias.swift | 4 +- .../Documentation.docc/MarkdownBasics.md | 74 +++ StructuredData.swift | 506 ------------------ .../Styles/InteractionModifiersTests.swift | 37 +- .../Styles/NewResponsiveTests.swift | 198 ------- .../Styles/ResponsiveDSLTests.swift | 147 ----- .../Styles/ResponsiveStyleBuilderTests.swift | 219 -------- Tests/WebUITests/Styles/ResponsiveTests.swift | 156 ++---- examples/InteractionModifiersExample.swift | 61 ++- 29 files changed, 1538 insertions(+), 1648 deletions(-) create mode 100644 Sources/WebUI/Documentation.docc/CoreConcepts.md create mode 100644 Sources/WebUI/Documentation.docc/CustomElements.md create mode 100644 Sources/WebUI/Documentation.docc/FormElements.md create mode 100644 Sources/WebUI/Documentation.docc/GettingStarted.md create mode 100644 Sources/WebUI/Documentation.docc/ResponsiveDesign.md create mode 100644 Sources/WebUI/Documentation.docc/SEOGuide.md create mode 100644 Sources/WebUI/Documentation.docc/StylingGuide.md delete mode 100644 Sources/WebUI/Documentation.docc/getting-started.md delete mode 100644 Sources/WebUI/Documentation.docc/styles.md delete mode 100644 Sources/WebUI/Documentation.docc/table-of-contents.tutorial create mode 100644 Sources/WebUIMarkdown/Documentation.docc/MarkdownBasics.md delete mode 100644 StructuredData.swift delete mode 100644 Tests/WebUITests/Styles/NewResponsiveTests.swift delete mode 100644 Tests/WebUITests/Styles/ResponsiveDSLTests.swift delete mode 100644 Tests/WebUITests/Styles/ResponsiveStyleBuilderTests.swift diff --git a/Sources/WebUI/Core/Metadata/Metadata.swift b/Sources/WebUI/Core/Metadata/Metadata.swift index 20181411..be1e5994 100644 --- a/Sources/WebUI/Core/Metadata/Metadata.swift +++ b/Sources/WebUI/Core/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/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/Styles/Appearance/Display/FlexStyleOperation.swift b/Sources/WebUI/Styles/Appearance/Display/FlexStyleOperation.swift index f40e74bd..0b0119b3 100644 --- a/Sources/WebUI/Styles/Appearance/Display/FlexStyleOperation.swift +++ b/Sources/WebUI/Styles/Appearance/Display/FlexStyleOperation.swift @@ -9,16 +9,16 @@ public struct FlexStyleOperation: StyleOperation, @unchecked Sendable { public struct Parameters { /// The flex direction (row, column, etc.) public let direction: FlexDirection? - + /// The justify content property (start, center, between, etc.) public let justify: FlexJustify? - + /// The align items property (start, center, end, etc.) public let align: FlexAlign? - + /// The flex grow property public let grow: FlexGrow? - + /// Creates parameters for flex styling /// /// - Parameters: @@ -37,7 +37,7 @@ public struct FlexStyleOperation: StyleOperation, @unchecked Sendable { self.align = align self.grow = grow } - + /// Creates parameters from a StyleParameters container /// /// - Parameter params: The style parameters container @@ -51,36 +51,36 @@ public struct FlexStyleOperation: StyleOperation, @unchecked Sendable { ) } } - + /// Applies the flex style and returns the appropriate CSS classes /// /// - Parameter params: The parameters for flex styling /// - Returns: An array of CSS class names to be applied to elements public func applyClasses(params: Parameters) -> [String] { var classes = ["flex"] - + if let direction = params.direction { classes.append("flex-\(direction.rawValue)") } - + if let justify = params.justify { classes.append("justify-\(justify.rawValue)") } - + if let align = params.align { classes.append("items-\(align.rawValue)") } - + if let grow = params.grow { classes.append("flex-\(grow.rawValue)") } - + return classes } - + /// Shared instance for use across the framework public static let shared = FlexStyleOperation() - + /// Private initializer to enforce singleton usage private init() {} } @@ -89,13 +89,13 @@ public struct FlexStyleOperation: StyleOperation, @unchecked Sendable { public enum FlexDirection: String { /// Items are arranged horizontally (left to right) case row - + /// Items are arranged horizontally in reverse (right to left) case rowReverse = "row-reverse" - + /// Items are arranged vertically (top to bottom) case column = "col" - + /// Items are arranged vertically in reverse (bottom to top) case columnReverse = "col-reverse" } @@ -104,19 +104,19 @@ public enum FlexDirection: String { public enum FlexJustify: String { /// Items are packed at the start of the container case start - + /// Items are packed at the end of the container case end - + /// Items are centered along the line case center - + /// Items are evenly distributed with equal space between them case between - + /// Items are evenly distributed with equal space around them case around - + /// Items are evenly distributed with equal space between and around them case evenly } @@ -125,16 +125,16 @@ public enum FlexJustify: String { public enum FlexAlign: String { /// Items are aligned at the start of the cross axis case start - + /// Items are aligned at the end of the cross axis case end - + /// Items are centered along the cross axis case center - + /// Items are stretched to fill the container case stretch - + /// Items are aligned at the baseline case baseline } @@ -143,19 +143,19 @@ public enum FlexAlign: String { public enum FlexGrow: String { /// No growing case none = "0" - + /// Grow with factor 1 case one = "1" - + /// Grow with factor 2 case two = "2" - + /// Grow with factor 3 case three = "3" - + /// Grow with factor 4 case four = "4" - + /// Grow with factor 5 case five = "5" } @@ -196,7 +196,7 @@ extension Element { align: align, grow: grow ) - + return FlexStyleOperation.shared.applyToElement( self, params: params, @@ -228,7 +228,7 @@ extension ResponsiveBuilder { align: align, grow: grow ) - + return FlexStyleOperation.shared.applyToBuilder(self, params: params) } } @@ -254,6 +254,6 @@ public func flex( align: align, grow: grow ) - + return FlexStyleOperation.shared.asModification(params: params) -} \ No newline at end of file +} diff --git a/Sources/WebUI/Styles/Appearance/Display/GridStyleOperation.swift b/Sources/WebUI/Styles/Appearance/Display/GridStyleOperation.swift index 10a2d4c4..71bb9090 100644 --- a/Sources/WebUI/Styles/Appearance/Display/GridStyleOperation.swift +++ b/Sources/WebUI/Styles/Appearance/Display/GridStyleOperation.swift @@ -9,19 +9,19 @@ public struct GridStyleOperation: StyleOperation, @unchecked Sendable { public struct Parameters { /// The number of grid columns public let columns: Int? - + /// The number of grid rows public let rows: Int? - + /// The grid flow direction public let flow: GridFlow? - + /// The column span value public let columnSpan: Int? - + /// The row span value public let rowSpan: Int? - + /// Creates parameters for grid styling /// /// - Parameters: @@ -43,7 +43,7 @@ public struct GridStyleOperation: StyleOperation, @unchecked Sendable { self.columnSpan = columnSpan self.rowSpan = rowSpan } - + /// Creates parameters from a StyleParameters container /// /// - Parameter params: The style parameters container @@ -58,40 +58,40 @@ public struct GridStyleOperation: StyleOperation, @unchecked Sendable { ) } } - + /// Applies the grid style and returns the appropriate CSS classes /// /// - Parameter params: The parameters for grid styling /// - Returns: An array of CSS class names to be applied to elements public func applyClasses(params: Parameters) -> [String] { var classes = ["grid"] - + if let columns = params.columns { classes.append("grid-cols-\(columns)") } - + if let rows = params.rows { classes.append("grid-rows-\(rows)") } - + if let flow = params.flow { classes.append("grid-flow-\(flow.rawValue)") } - + if let columnSpan = params.columnSpan { classes.append("col-span-\(columnSpan)") } - + if let rowSpan = params.rowSpan { classes.append("row-span-\(rowSpan)") } - + return classes } - + /// Shared instance for use across the framework public static let shared = GridStyleOperation() - + /// Private initializer to enforce singleton usage private init() {} } @@ -100,13 +100,13 @@ public struct GridStyleOperation: StyleOperation, @unchecked Sendable { public enum GridFlow: String { /// Items flow row by row case row - + /// Items flow column by column case col - + /// Items flow row by row, dense packing case rowDense = "row-dense" - + /// Items flow column by column, dense packing case colDense = "col-dense" } @@ -150,7 +150,7 @@ extension Element { columnSpan: columnSpan, rowSpan: rowSpan ) - + return GridStyleOperation.shared.applyToElement( self, params: params, @@ -185,7 +185,7 @@ extension ResponsiveBuilder { columnSpan: columnSpan, rowSpan: rowSpan ) - + return GridStyleOperation.shared.applyToBuilder(self, params: params) } } @@ -214,6 +214,6 @@ public func grid( columnSpan: columnSpan, rowSpan: rowSpan ) - + return GridStyleOperation.shared.asModification(params: params) -} \ No newline at end of file +} diff --git a/Sources/WebUI/Styles/Appearance/Display/VisibilityStyleOperation.swift b/Sources/WebUI/Styles/Appearance/Display/VisibilityStyleOperation.swift index 919f01fd..b9b3b08a 100644 --- a/Sources/WebUI/Styles/Appearance/Display/VisibilityStyleOperation.swift +++ b/Sources/WebUI/Styles/Appearance/Display/VisibilityStyleOperation.swift @@ -9,14 +9,14 @@ public struct VisibilityStyleOperation: StyleOperation, @unchecked Sendable { public struct Parameters { /// Whether the element should be hidden public let isHidden: Bool - + /// Creates parameters for visibility styling /// /// - Parameter isHidden: Whether the element should be hidden public init(isHidden: Bool = true) { self.isHidden = isHidden } - + /// Creates parameters from a StyleParameters container /// /// - Parameter params: The style parameters container @@ -27,7 +27,7 @@ public struct VisibilityStyleOperation: StyleOperation, @unchecked Sendable { ) } } - + /// Applies the visibility style and returns the appropriate CSS classes /// /// - Parameter params: The parameters for visibility styling @@ -39,10 +39,10 @@ public struct VisibilityStyleOperation: StyleOperation, @unchecked Sendable { return [] } } - + /// Shared instance for use across the framework public static let shared = VisibilityStyleOperation() - + /// Private initializer to enforce singleton usage private init() {} } @@ -75,7 +75,7 @@ extension Element { on modifiers: Modifier... ) -> Element { let params = VisibilityStyleOperation.Parameters(isHidden: isHidden) - + return VisibilityStyleOperation.shared.applyToElement( self, params: params, @@ -93,7 +93,7 @@ extension ResponsiveBuilder { @discardableResult public func hidden(_ isHidden: Bool = true) -> ResponsiveBuilder { let params = VisibilityStyleOperation.Parameters(isHidden: isHidden) - + return VisibilityStyleOperation.shared.applyToBuilder(self, params: params) } } @@ -105,6 +105,6 @@ extension ResponsiveBuilder { /// - Returns: A responsive modification for visibility. public func hidden(_ isHidden: Bool = true) -> ResponsiveModification { let params = VisibilityStyleOperation.Parameters(isHidden: isHidden) - + return VisibilityStyleOperation.shared.asModification(params: params) -} \ No newline at end of file +} diff --git a/Sources/WebUI/Styles/Appearance/OutlineStyleOperation.swift b/Sources/WebUI/Styles/Appearance/OutlineStyleOperation.swift index 5d279fc7..6539a36b 100644 --- a/Sources/WebUI/Styles/Appearance/OutlineStyleOperation.swift +++ b/Sources/WebUI/Styles/Appearance/OutlineStyleOperation.swift @@ -9,16 +9,16 @@ public struct OutlineStyleOperation: StyleOperation, @unchecked Sendable { public struct Parameters { /// The outline width public let width: Int? - + /// The outline style (solid, dashed, etc.) public let style: BorderStyle? - + /// The outline color public let color: Color? - + /// The outline offset public let offset: Int? - + /// Creates parameters for outline styling /// /// - Parameters: @@ -37,7 +37,7 @@ public struct OutlineStyleOperation: StyleOperation, @unchecked Sendable { self.color = color self.offset = offset } - + /// Creates parameters from a StyleParameters container /// /// - Parameter params: The style parameters container @@ -51,40 +51,40 @@ public struct OutlineStyleOperation: StyleOperation, @unchecked Sendable { ) } } - + /// Applies the outline style and returns the appropriate CSS classes /// /// - Parameter params: The parameters for outline styling /// - Returns: An array of CSS class names to be applied to elements public func applyClasses(params: Parameters) -> [String] { var classes = [String]() - + if let width = params.width { classes.append("outline-\(width)") } - + if let style = params.style { classes.append("outline-\(style.rawValue)") } - + if let color = params.color { classes.append("outline-\(color.rawValue)") } - + if let offset = params.offset { classes.append("outline-offset-\(offset)") } - + if classes.isEmpty { classes.append("outline") } - + return classes } - + /// Shared instance for use across the framework public static let shared = OutlineStyleOperation() - + /// Private initializer to enforce singleton usage private init() {} } @@ -125,7 +125,7 @@ extension Element { color: color, offset: offset ) - + return OutlineStyleOperation.shared.applyToElement( self, params: params, @@ -157,7 +157,7 @@ extension ResponsiveBuilder { color: color, offset: offset ) - + return OutlineStyleOperation.shared.applyToBuilder(self, params: params) } } @@ -183,6 +183,6 @@ public func outline( color: color, offset: offset ) - + return OutlineStyleOperation.shared.asModification(params: params) -} \ No newline at end of file +} diff --git a/Sources/WebUI/Styles/Appearance/RingStyleOperation.swift b/Sources/WebUI/Styles/Appearance/RingStyleOperation.swift index 7fe91b7b..1761cc97 100644 --- a/Sources/WebUI/Styles/Appearance/RingStyleOperation.swift +++ b/Sources/WebUI/Styles/Appearance/RingStyleOperation.swift @@ -77,9 +77,7 @@ extension Element { /// Adds rings with custom width, style, and color to specified edges of an element. /// /// - Parameters: - /// - width: The ring width in pixels. - /// - edges: One or more edges to apply the ring to. Defaults to `.all`. - /// - style: The ring style (solid, dashed, etc.). + /// - size: The width of the ring. /// - color: The ring color. /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. /// - Returns: A new element with updated ring classes. diff --git a/Sources/WebUI/Styles/Base/Sizing/SizingStyleOperation.swift b/Sources/WebUI/Styles/Base/Sizing/SizingStyleOperation.swift index 54b9c2f8..2d3fa101 100644 --- a/Sources/WebUI/Styles/Base/Sizing/SizingStyleOperation.swift +++ b/Sources/WebUI/Styles/Base/Sizing/SizingStyleOperation.swift @@ -155,7 +155,7 @@ public struct SizingStyleOperation: StyleOperation, @unchecked Sendable { return classes } - + public func applyFrameClasses(params: FrameParameters) -> [String] { applyClasses(params: params) } diff --git a/Sources/WebUI/Styles/Base/Utilities.swift b/Sources/WebUI/Styles/Base/Utilities.swift index 24f6d953..a4068316 100644 --- a/Sources/WebUI/Styles/Base/Utilities.swift +++ b/Sources/WebUI/Styles/Base/Utilities.swift @@ -77,62 +77,62 @@ public enum Modifier: String { /// /// Use to style the first item in a list or container. case first - + /// Applies the style to the last child element. /// /// Use to style the last item in a list or container. case last - + /// Applies the style when the element is disabled. /// /// Use to provide visual feedback for disabled form elements and controls. case disabled - + /// Applies the style when the user prefers reduced motion. /// /// Use to create alternative animations or transitions for users who prefer reduced motion. case motionReduce = "motion-reduce" - + /// Applies the style when the element has aria-busy="true". /// /// Use to style elements that are in a busy or loading state. case ariaBusy = "aria-busy" - + /// Applies the style when the element has aria-checked="true". /// /// Use to style elements that represent a checked state, like checkboxes. case ariaChecked = "aria-checked" - + /// Applies the style when the element has aria-disabled="true". /// /// Use to style elements that are disabled via ARIA attributes. case ariaDisabled = "aria-disabled" - + /// Applies the style when the element has aria-expanded="true". /// /// Use to style elements that can be expanded, like accordions or dropdowns. case ariaExpanded = "aria-expanded" - + /// Applies the style when the element has aria-hidden="true". /// /// Use to style elements that are hidden from screen readers. case ariaHidden = "aria-hidden" - + /// Applies the style when the element has aria-pressed="true". /// /// Use to style elements that represent a pressed state, like toggle buttons. case ariaPressed = "aria-pressed" - + /// Applies the style when the element has aria-readonly="true". /// /// Use to style elements that are in a read-only state. case ariaReadonly = "aria-readonly" - + /// Applies the style when the element has aria-required="true". /// /// Use to style elements that are required, like form inputs. case ariaRequired = "aria-required" - + /// Applies the style when the element has aria-selected="true". /// /// Use to style elements that are in a selected state, like tabs or menu items. diff --git a/Sources/WebUI/Styles/Core/InteractionModifiers.swift b/Sources/WebUI/Styles/Core/InteractionModifiers.swift index ed1f2f1e..ed268a76 100644 --- a/Sources/WebUI/Styles/Core/InteractionModifiers.swift +++ b/Sources/WebUI/Styles/Core/InteractionModifiers.swift @@ -17,7 +17,7 @@ extension ResponsiveBuilder { applyBreakpoint() return self } - + /// Applies styles when the element has keyboard focus. /// /// - Parameter modifications: A closure containing style modifications. @@ -29,7 +29,7 @@ extension ResponsiveBuilder { applyBreakpoint() return self } - + /// Applies styles when the element is being actively pressed or clicked. /// /// - Parameter modifications: A closure containing style modifications. @@ -41,7 +41,7 @@ extension ResponsiveBuilder { applyBreakpoint() return self } - + /// Applies styles to input placeholders within the element. /// /// - Parameter modifications: A closure containing style modifications. @@ -53,7 +53,7 @@ extension ResponsiveBuilder { applyBreakpoint() return self } - + /// Applies styles when dark mode is active. /// /// - Parameter modifications: A closure containing style modifications. @@ -65,7 +65,7 @@ extension ResponsiveBuilder { applyBreakpoint() return self } - + /// Applies styles to the first child element. /// /// - Parameter modifications: A closure containing style modifications. @@ -77,7 +77,7 @@ extension ResponsiveBuilder { applyBreakpoint() return self } - + /// Applies styles to the last child element. /// /// - Parameter modifications: A closure containing style modifications. @@ -89,7 +89,7 @@ extension ResponsiveBuilder { applyBreakpoint() return self } - + /// Applies styles when the element is disabled. /// /// - Parameter modifications: A closure containing style modifications. @@ -101,7 +101,7 @@ extension ResponsiveBuilder { applyBreakpoint() return self } - + /// Applies styles when the user prefers reduced motion. /// /// - Parameter modifications: A closure containing style modifications. @@ -113,7 +113,7 @@ extension ResponsiveBuilder { applyBreakpoint() return self } - + /// Applies styles when the element has aria-busy="true". /// /// - Parameter modifications: A closure containing style modifications. @@ -125,7 +125,7 @@ extension ResponsiveBuilder { applyBreakpoint() return self } - + /// Applies styles when the element has aria-checked="true". /// /// - Parameter modifications: A closure containing style modifications. @@ -137,7 +137,7 @@ extension ResponsiveBuilder { applyBreakpoint() return self } - + /// Applies styles when the element has aria-disabled="true". /// /// - Parameter modifications: A closure containing style modifications. @@ -149,7 +149,7 @@ extension ResponsiveBuilder { applyBreakpoint() return self } - + /// Applies styles when the element has aria-expanded="true". /// /// - Parameter modifications: A closure containing style modifications. @@ -161,7 +161,7 @@ extension ResponsiveBuilder { applyBreakpoint() return self } - + /// Applies styles when the element has aria-hidden="true". /// /// - Parameter modifications: A closure containing style modifications. @@ -173,7 +173,7 @@ extension ResponsiveBuilder { applyBreakpoint() return self } - + /// Applies styles when the element has aria-pressed="true". /// /// - Parameter modifications: A closure containing style modifications. @@ -185,7 +185,7 @@ extension ResponsiveBuilder { applyBreakpoint() return self } - + /// Applies styles when the element has aria-readonly="true". /// /// - Parameter modifications: A closure containing style modifications. @@ -197,7 +197,7 @@ extension ResponsiveBuilder { applyBreakpoint() return self } - + /// Applies styles when the element has aria-required="true". /// /// - Parameter modifications: A closure containing style modifications. @@ -209,7 +209,7 @@ extension ResponsiveBuilder { applyBreakpoint() return self } - + /// Applies styles when the element has aria-selected="true". /// /// - Parameter modifications: A closure containing style modifications. @@ -367,4 +367,4 @@ public func ariaRequired(@ResponsiveStyleBuilder content: () -> ResponsiveModifi /// - Returns: A responsive modification for the aria-selected state. public func ariaSelected(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { BreakpointModification(breakpoint: .ariaSelected, styleModification: content()) -} \ No newline at end of file +} diff --git a/Sources/WebUI/Styles/Core/ResponsiveAlias.swift b/Sources/WebUI/Styles/Core/ResponsiveAlias.swift index 2c0972b1..0ea440fe 100644 --- a/Sources/WebUI/Styles/Core/ResponsiveAlias.swift +++ b/Sources/WebUI/Styles/Core/ResponsiveAlias.swift @@ -10,6 +10,6 @@ extension Element { /// - Parameter content: A closure defining responsive style configurations using the result builder. /// - Returns: An element with responsive styles applied. public func responsive(@ResponsiveStyleBuilder _ content: () -> ResponsiveModification) -> Element { - return on(content) + on(content) } -} \ No newline at end of file +} diff --git a/Sources/WebUIMarkdown/Documentation.docc/MarkdownBasics.md b/Sources/WebUIMarkdown/Documentation.docc/MarkdownBasics.md new file mode 100644 index 00000000..5da2b889 --- /dev/null +++ b/Sources/WebUIMarkdown/Documentation.docc/MarkdownBasics.md @@ -0,0 +1,74 @@ +# Markdown Basics + +Learn how to use WebUIMarkdown to render Markdown content in your web applications. + +## Overview + +WebUIMarkdown provides seamless integration of Markdown content within your WebUI applications. + +## Basic Usage + +```swift +import WebUI +import WebUIMarkdown + +struct BlogPost: Website { + var body: some HTML { + Document { + Markdown { + """ + # Welcome to my blog + + This is a paragraph with **bold** and *italic* text. + + - List item 1 + - List item 2 + """ + } + } + } +} +``` + +## Features + +### Supported Markdown Elements + +- Headers (H1-H6) +- Emphasis (bold, italic) +- Lists (ordered and unordered) +- Links and images +- Code blocks with syntax highlighting +- Tables +- Blockquotes +- Task lists + +### Syntax Highlighting + +WebUIMarkdown includes built-in syntax highlighting for code blocks: + +```swift +Markdown { + """ + ```swift + func hello() { + print("Hello, World!") + } + ``` + """ +} +.syntaxHighlighting(.github) +``` + +## Topics + +### Getting Started + +- +- + +### Advanced Features + +- +- ``MarkdownParser`` +- ``HighlightingOptions`` diff --git a/StructuredData.swift b/StructuredData.swift deleted file mode 100644 index a9a54c09..00000000 --- a/StructuredData.swift +++ /dev/null @@ -1,506 +0,0 @@ -import Foundation - -/// Represents structured data in JSON-LD format for rich snippets in search results. -/// -/// The `StructuredData` struct provides a type-safe way to define structured data -/// following various schema.org schemas like Article, Product, Organization, etc. -public struct StructuredData { - - /// The schema type for the structured data. - public enum SchemaType: String { - case article = "Article" - case blogPosting = "BlogPosting" - case breadcrumbList = "BreadcrumbList" - case course = "Course" - case event = "Event" - case faqPage = "FAQPage" - case howTo = "HowTo" - case localBusiness = "LocalBusiness" - case organization = "Organization" - case person = "Person" - case product = "Product" - case recipe = "Recipe" - case review = "Review" - case website = "WebSite" - } - - /// The type of schema used for this structured data. - public let type: SchemaType - - /// The raw data to be included in the structured data. - private let data: [String: Any] - - /// Returns a copy of the raw data dictionary. - /// - /// - Returns: A dictionary containing the structured data properties. - public func getData() -> [String: Any] { - data - } - - /// Creates structured data for an Article. - /// - /// - Parameters: - /// - headline: The title of the article. - /// - image: The URL to the featured image of the article. - /// - author: The name or URL of the author. - /// - publisher: Optional publisher, either as a StructuredData person or organization, or as a String name. - /// - datePublished: The date the article was published. - /// - dateModified: The date the article was last modified. - /// - description: A short description of the article content. - /// - url: The URL of the article. - /// - Returns: A structured data object for an article. - /// - /// - Example: - /// ```swift - /// // Using a String for publisher - /// let articleData = StructuredData.article( - /// headline: "How to Use WebUI", - /// image: "https://example.com/images/article.jpg", - /// author: "John Doe", - /// publisher: "WebUI Blog", - /// datePublished: Date(), - /// description: "A guide to using WebUI for Swift developers" - /// ) - /// - /// // Using a StructuredData organization as publisher - /// let orgPublisher = StructuredData.organization( - /// name: "WebUI Technologies", - /// logo: "https://example.com/logo.png", - /// url: "https://example.com" - /// ) - /// - /// let articleWithOrg = StructuredData.article( - /// headline: "How to Use WebUI", - /// image: "https://example.com/images/article.jpg", - /// author: "John Doe", - /// publisher: orgPublisher, - /// datePublished: Date(), - /// description: "A guide to using WebUI for Swift developers" - /// ) - /// - /// // Without a publisher - /// let minimalArticle = StructuredData.article( - /// headline: "Quick Tips", - /// image: "https://example.com/images/tips.jpg", - /// author: "Alex Developer", - /// datePublished: Date() - /// ) - /// ``` - /// - /// - Note: For more control over the publisher entity, use the overloaded version - /// of this method that accepts a StructuredData object as the publisher parameter. - public static func article( - headline: String, - image: String, - author: String, - publisher: Any? = nil, - datePublished: Date, - dateModified: Date? = nil, - description: String? = nil, - url: String? = nil - ) -> StructuredData { - var data: [String: Any] = [ - "headline": headline, - "image": image, - "author": ["@type": "Person", "name": author], - "publisher": ["@type": "Organization", "name": publisher], - "datePublished": ISO8601DateFormatter().string(from: datePublished), - ] - - // Handle different publisher types - if let publisher = publisher { - if let publisherName = publisher as? String { - data["publisher"] = ["@type": "Organization", "name": publisherName] - } else if let publisherData = publisher as? StructuredData { - if publisherData.type == .organization || publisherData.type == .person { - // Extract the raw data from the structured data object - let publisherDict = publisherData.getData() - var typeDict = publisherDict - typeDict["@type"] = publisherData.type.rawValue - data["publisher"] = typeDict - } - } - } - - if let dateModified = dateModified { - data["dateModified"] = ISO8601DateFormatter().string(from: dateModified) - } - - if let description = description { - data["description"] = description - } - - if let url = url { - data["url"] = url - } - - return StructuredData(type: .article, data: data) - } - - /// Creates structured data for a product. - /// - /// - Parameters: - /// - name: The name of the product. - /// - image: The URL to the product image. - /// - description: A description of the product. - /// - sku: The Stock Keeping Unit identifier. - /// - brand: The brand name of the product. - /// - offers: The offer details (price, availability, etc.). - /// - review: Optional review information. - /// - Returns: A structured data object for a product. - /// - /// - Example: - /// ```swift - /// let productData = StructuredData.product( - /// name: "Swift WebUI Course", - /// image: "https://example.com/images/course.jpg", - /// description: "Master WebUI development with Swift", - /// sku: "WEBUI-101", - /// brand: "Swift Academy", - /// offers: ["price": "99.99", "priceCurrency": "USD", "availability": "InStock"] - /// ) - /// ``` - public static func product( - name: String, - image: String, - description: String, - sku: String, - brand: String, - offers: [String: Any], - review: [String: Any]? = nil - ) -> StructuredData { - var data: [String: Any] = [ - "name": name, - "image": image, - "description": description, - "sku": sku, - "brand": ["@type": "Brand", "name": brand], - "offers": offers.merging(["@type": "Offer"]) { _, new in new }, - ] - - if let review = review { - data["review"] = review.merging(["@type": "Review"]) { _, new in new } - } - - return StructuredData(type: .product, data: data) - } - - /// Creates structured data for an organization. - /// - /// - Parameters: - /// - name: The name of the organization. - /// - logo: The URL to the organization's logo. - /// - url: The URL of the organization's website. - /// - contactPoint: Optional contact information. - /// - sameAs: Optional array of URLs that also represent the entity. - /// - Returns: A structured data object for an organization. - /// - /// - Example: - /// ```swift - /// let orgData = StructuredData.organization( - /// name: "WebUI Technologies", - /// logo: "https://example.com/logo.png", - /// url: "https://example.com", - /// sameAs: ["https://twitter.com/webui", "https://github.com/webui"] - /// ) - /// ``` - public static func organization( - name: String, - logo: String, - url: String, - contactPoint: [String: Any]? = nil, - sameAs: [String]? = nil - ) -> StructuredData { - var data: [String: Any] = [ - "name": name, - "logo": logo, - "url": url, - ] - - if let contactPoint = contactPoint { - data["contactPoint"] = contactPoint.merging(["@type": "ContactPoint"]) { _, new in new } - } - - if let sameAs = sameAs { - data["sameAs"] = sameAs - } - - return StructuredData(type: .organization, data: data) - } - - /// Creates structured data for a person. - /// - /// - Parameters: - /// - name: The name of the person. - /// - givenName: The given (first) name of the person. - /// - familyName: The family (last) name of the person. - /// - image: The URL to an image of the person. - /// - jobTitle: The person's job title. - /// - email: The person's email address. - /// - telephone: The person's telephone number. - /// - url: The URL of the person's website or profile. - /// - address: Optional address information. - /// - birthDate: The person's date of birth. - /// - sameAs: Optional array of URLs that also represent the person (social profiles). - /// - Returns: A structured data object for a person. - /// - /// - Example: - /// ```swift - /// let personData = StructuredData.person( - /// name: "Jane Doe", - /// givenName: "Jane", - /// familyName: "Doe", - /// jobTitle: "Software Engineer", - /// url: "https://janedoe.com", - /// sameAs: ["https://twitter.com/janedoe", "https://github.com/janedoe"] - /// ) - /// ``` - public static func person( - name: String, - givenName: String? = nil, - familyName: String? = nil, - image: String? = nil, - jobTitle: String? = nil, - email: String? = nil, - telephone: String? = nil, - url: String? = nil, - address: [String: Any]? = nil, - birthDate: Date? = nil, - sameAs: [String]? = nil - ) -> StructuredData { - var data: [String: Any] = [ - "name": name - ] - - if let givenName = givenName { - data["givenName"] = givenName - } - - if let familyName = familyName { - data["familyName"] = familyName - } - - if let image = image { - data["image"] = image - } - - if let jobTitle = jobTitle { - data["jobTitle"] = jobTitle - } - - if let email = email { - data["email"] = email - } - - if let telephone = telephone { - data["telephone"] = telephone - } - - if let url = url { - data["url"] = url - } - - if let address = address { - data["address"] = address.merging(["@type": "PostalAddress"]) { _, new in new } - } - - if let birthDate = birthDate { - data["birthDate"] = ISO8601DateFormatter().string(from: birthDate) - } - - if let sameAs = sameAs { - data["sameAs"] = sameAs - } - - return StructuredData(type: .person, data: data) - } - - /// Creates structured data for breadcrumbs navigation. - /// - /// - Parameter items: Array of breadcrumb items with name, item (URL), and position. - /// - Returns: A structured data object for breadcrumbs navigation. - /// - /// - Example: - /// ```swift - /// let breadcrumbsData = StructuredData.breadcrumbs([ - /// ["name": "Home", "item": "https://example.com", "position": 1], - /// ["name": "Blog", "item": "https://example.com/blog", "position": 2], - /// ["name": "Article Title", "item": "https://example.com/blog/article", "position": 3] - /// ]) - /// ``` - public static func breadcrumbs(_ items: [[String: Any]]) -> StructuredData { - let itemListElements = items.map { item in - var element: [String: Any] = ["@type": "ListItem"] - - if let name = item["name"] as? String { - element["name"] = name - } - - if let itemUrl = item["item"] as? String { - element["item"] = itemUrl - } - - if let position = item["position"] as? Int { - element["position"] = position - } - - return element - } - - return StructuredData(type: .breadcrumbList, data: ["itemListElement": itemListElements]) - } - - /// Creates structured data for an Article with a StructuredData publisher. - /// - /// - Parameters: - /// - headline: The title of the article. - /// - image: The URL to the featured image of the article. - /// - author: The name or URL of the author. - /// - publisher: Optional StructuredData object representing the publisher as a person or organization. - /// - datePublished: The date the article was published. - /// - dateModified: The date the article was last modified. - /// - description: A short description of the article content. - /// - url: The URL of the article. - /// - Returns: A structured data object for an article. - /// - /// - Example: - /// ```swift - /// // Using an organization as publisher - /// let organization = StructuredData.organization( - /// name: "WebUI Technologies", - /// logo: "https://example.com/logo.png", - /// url: "https://example.com" - /// ) - /// - /// let articleWithOrg = StructuredData.article( - /// headline: "How to Use WebUI", - /// image: "https://example.com/images/article.jpg", - /// author: "John Doe", - /// publisher: organization, - /// datePublished: Date(), - /// description: "A guide to using WebUI for Swift developers" - /// ) - /// - /// // Using a person as publisher - /// let personPublisher = StructuredData.person( - /// name: "Jane Doe", - /// url: "https://janedoe.com" - /// ) - /// - /// let articleWithPerson = StructuredData.article( - /// headline: "My WebUI Journey", - /// image: "https://example.com/images/journey.jpg", - /// author: "John Smith", - /// publisher: personPublisher, - /// datePublished: Date(), - /// description: "Personal experiences with WebUI framework" - /// ) - /// - /// // Without a publisher - /// let minimalArticle = StructuredData.article( - /// headline: "Quick Tips", - /// image: "https://example.com/images/tips.jpg", - /// author: "Alex Developer", - /// datePublished: Date() - /// ) - /// ``` - public static func article( - headline: String, - image: String, - author: String, - publisher: StructuredData?, - datePublished: Date, - dateModified: Date? = nil, - description: String? = nil, - url: String? = nil - ) -> StructuredData { - var data: [String: Any] = [ - "headline": headline, - "image": image, - "author": ["@type": "Person", "name": author], - "datePublished": ISO8601DateFormatter().string(from: datePublished), - ] - - if let publisher = publisher { - if publisher.type == .organization || publisher.type == .person { - // Extract the raw data from the structured data object - let publisherDict = publisher.getData() - var typeDict = publisherDict - typeDict["@type"] = publisher.type.rawValue - data["publisher"] = typeDict - } - } - - if let dateModified = dateModified { - data["dateModified"] = ISO8601DateFormatter().string(from: dateModified) - } - - if let description = description { - data["description"] = description - } - - if let url = url { - data["url"] = url - } - - return StructuredData(type: .article, data: data) - } - - /// Creates a custom structured data object with the specified schema type and data. - /// - /// - Parameters: - /// - type: The schema type for the structured data. - /// - data: The data to include in the structured data. - /// - Returns: A structured data object with the specified type and data. - /// - /// - Example: - /// ```swift - /// let customData = StructuredData.custom( - /// type: .review, - /// data: [ - /// "itemReviewed": ["@type": "Product", "name": "WebUI Framework"], - /// "reviewRating": ["@type": "Rating", "ratingValue": "5"], - /// "author": ["@type": "Person", "name": "Jane Developer"] - /// ] - /// ) - /// ``` - public static func custom(type: SchemaType, data: [String: Any]) -> StructuredData { - StructuredData(type: type, data: data) - } - - /// Initializes a new structured data object with the specified schema type and data. - /// - /// - Parameters: - /// - type: The schema type for the structured data. - /// - data: The data to include in the structured data. - public init(type: SchemaType, data: [String: Any]) { - self.type = type - self.data = data - } - - /// Converts the structured data to a JSON string. - /// - /// - Returns: A JSON string representation of the structured data, or an empty string if serialization fails. - public func toJSON() -> String { - var jsonObject: [String: Any] = [ - "@context": "https://schema.org", - "@type": type.rawValue, - ] - - // Merge the data dictionary with the base JSON object - for (key, value) in data { - jsonObject[key] = value - } - - // Try to serialize the JSON object to data - if let jsonData = try? JSONSerialization.data( - withJSONObject: jsonObject, - options: [.prettyPrinted, .withoutEscapingSlashes] - ) { - // Convert the data to a string - return String(data: jsonData, encoding: .utf8) ?? "" - } - - return "" - } -} diff --git a/Tests/WebUITests/Styles/InteractionModifiersTests.swift b/Tests/WebUITests/Styles/InteractionModifiersTests.swift index b383b16b..2face5b6 100644 --- a/Tests/WebUITests/Styles/InteractionModifiersTests.swift +++ b/Tests/WebUITests/Styles/InteractionModifiersTests.swift @@ -1,4 +1,5 @@ import Testing + @testable import WebUI @Suite("Interaction Modifiers Tests") @@ -12,12 +13,12 @@ struct InteractionModifiersTests { font(color: .gray(._50)) } } - + let rendered = element.render() #expect(rendered.contains("hover:bg-blue-500")) #expect(rendered.contains("hover:text-gray-50")) } - + @Test("Focus state modifier") func testFocusStateModifier() async throws { let element = Element(tag: "div") @@ -27,13 +28,13 @@ struct InteractionModifiersTests { outline(of: 0) } } - + let rendered = element.render() #expect(rendered.contains("focus:border-2")) #expect(rendered.contains("focus:border-blue-500")) #expect(rendered.contains("focus:outline-0")) } - + @Test("Multiple state modifiers") func testMultipleStateModifiers() async throws { let element = Element(tag: "div") @@ -51,7 +52,7 @@ struct InteractionModifiersTests { background(color: .blue(._200)) } } - + let rendered = element.render() #expect(rendered.contains("bg-gray-100")) #expect(rendered.contains("p-4")) @@ -61,7 +62,7 @@ struct InteractionModifiersTests { #expect(rendered.contains("focus:border-blue-500")) #expect(rendered.contains("active:bg-blue-200")) } - + @Test("ARIA state modifiers") func testAriaStateModifiers() async throws { let element = Element(tag: "div") @@ -74,14 +75,14 @@ struct InteractionModifiersTests { font(weight: .bold) } } - + let rendered = element.render() #expect(rendered.contains("aria-expanded:border-1")) #expect(rendered.contains("aria-expanded:border-gray-300")) #expect(rendered.contains("aria-selected:bg-blue-100")) #expect(rendered.contains("aria-selected:font-bold")) } - + @Test("Placeholder modifier") func testPlaceholderModifier() async throws { let element = Element(tag: "input") @@ -91,12 +92,12 @@ struct InteractionModifiersTests { font(weight: .light) } } - + let rendered = element.render() #expect(rendered.contains("placeholder:text-gray-400")) #expect(rendered.contains("placeholder:font-light")) } - + @Test("First and last child modifiers") func testFirstLastChildModifiers() async throws { let element = Element(tag: "ul") @@ -108,12 +109,12 @@ struct InteractionModifiersTests { border(of: 0, at: .bottom) } } - + let rendered = element.render() #expect(rendered.contains("first:border-t-0")) #expect(rendered.contains("last:border-b-0")) } - + @Test("Disabled state modifier") func testDisabledStateModifier() async throws { let element = Element(tag: "button") @@ -123,12 +124,12 @@ struct InteractionModifiersTests { cursor(.notAllowed) } } - + let rendered = element.render() #expect(rendered.contains("disabled:opacity-50")) #expect(rendered.contains("disabled:cursor-not-allowed")) } - + @Test("Motion reduce modifier") func testMotionReduceModifier() async throws { let element = Element(tag: "div") @@ -138,13 +139,13 @@ struct InteractionModifiersTests { transition(of: .transform, for: 0) } } - + let rendered = element.render() #expect(rendered.contains("transition-transform")) #expect(rendered.contains("duration-300")) #expect(rendered.contains("motion-reduce:duration-0")) } - + @Test("Complex interactive button") func testComplexInteractiveButton() async throws { let button = Element(tag: "button") @@ -172,7 +173,7 @@ struct InteractionModifiersTests { cursor(.notAllowed) } } - + let rendered = button.render() #expect(rendered.contains("p-4")) #expect(rendered.contains("bg-blue-500")) @@ -193,4 +194,4 @@ struct InteractionModifiersTests { #expect(rendered.contains("disabled:opacity-75")) #expect(rendered.contains("disabled:cursor-not-allowed")) } -} \ No newline at end of file +} diff --git a/Tests/WebUITests/Styles/NewResponsiveTests.swift b/Tests/WebUITests/Styles/NewResponsiveTests.swift deleted file mode 100644 index d39e80c5..00000000 --- a/Tests/WebUITests/Styles/NewResponsiveTests.swift +++ /dev/null @@ -1,198 +0,0 @@ -import Testing - -@testable import WebUI - -@Suite("New Responsive Style Tests") struct NewResponsiveTests { - // MARK: - Basic Responsive Tests - - @Test("Basic responsive font styling with result builder syntax") - func testBasicResponsiveFontStylingNewSyntax() async throws { - let element = Element(tag: "div") - .font(size: .sm) - .responsive { - md { - font(size: .lg) - } - } - - let rendered = element.render() - #expect(rendered.contains("class=\"text-sm md:text-lg\"")) - } - - @Test("Multiple breakpoints with result builder syntax") - func testMultipleBreakpointsNewSyntax() async throws { - let element = Element(tag: "div") - .background(color: .gray(._100)) - .font(size: .sm) - .on { - sm { - font(size: .base) - } - md { - font(size: .lg) - background(color: .gray(._200)) - } - lg { - font(size: .xl) - background(color: .gray(._300)) - } - } - - let rendered = element.render() - #expect( - rendered.contains( - "class=\"bg-gray-100 text-sm sm:text-base md:text-lg md:bg-gray-200 lg:text-xl lg:bg-gray-300\"" - ) - ) - } - - // MARK: - Multiple Style Types Tests - - @Test("Multiple style types with result builder syntax") - func testMultipleStyleTypesNewSyntax() async throws { - let element = Element(tag: "div") - .padding(of: 2) - .font(size: .sm) - .on { - md { - padding(of: 4) - font(size: .lg) - margins(of: 2) - } - } - - let rendered = element.render() - #expect(rendered.contains("class=\"p-2 text-sm md:p-4 md:text-lg md:m-2\"")) - } - - // MARK: - Complex Component Tests - - @Test("Complex component with result builder syntax") - func testComplexComponentNewSyntax() async throws { - let button = Button(type: .submit) { "Submit" } - .background(color: .blue(._500)) - .font(color: .blue(._50)) - .padding(of: 2) - .rounded(.md) - .on { - sm { - padding(of: 3) - } - md { - padding(of: 4) - font(size: .lg) - } - lg { - padding(of: 6) - background(color: .blue(._600)) - } - } - - let rendered = button.render() - #expect(rendered.contains("type=\"submit\"")) - #expect(rendered.contains("bg-blue-500")) - #expect(rendered.contains("text-blue-50")) - #expect(rendered.contains("p-2")) - #expect(rendered.contains("rounded-md")) - #expect(rendered.contains("sm:p-3")) - #expect(rendered.contains("md:p-4")) - #expect(rendered.contains("md:text-lg")) - #expect(rendered.contains("lg:p-6")) - #expect(rendered.contains("lg:bg-blue-600")) - } - - // MARK: - Layout Tests - - @Test("Responsive flex layout with result builder syntax") - func testResponsiveFlexLayoutNewSyntax() async throws { - let element = Element(tag: "div") - .flex(direction: .column) - .on { - md { - flex(direction: .row, justify: .between) - } - } - - let rendered = element.render() - #expect(rendered.contains("flex")) - #expect(rendered.contains("flex-col")) - #expect(rendered.contains("md:flex")) - #expect(rendered.contains("md:flex-row") || rendered.contains("md:row")) - #expect(rendered.contains("md:justify-between")) - } - - @Test("Responsive grid layout with result builder syntax") - func testResponsiveGridLayoutNewSyntax() async throws { - let element = Element(tag: "div") - .grid(columns: 1) - .on { - md { - grid(columns: 2) - } - lg { - grid(columns: 3) - } - } - - let rendered = element.render() - #expect(rendered.contains("grid")) - #expect(rendered.contains("grid-cols-1")) - #expect(rendered.contains("md:grid-cols-2")) - #expect(rendered.contains("lg:grid-cols-3")) - } - - // MARK: - Visibility Tests - - @Test("Responsive visibility with result builder syntax") - func testResponsiveVisibilityNewSyntax() async throws { - let element = Element(tag: "div") - .hidden() - .on { - md { - hidden(false) - } - } - - let rendered = element.render() - #expect(rendered.contains("class=\"hidden\"")) - #expect(!rendered.contains("md:hidden")) - } - - // MARK: - Backward Compatibility Test - - @Test("Test all breakpoint modifiers") - func testAllBreakpointModifiers() async throws { - let element = Element(tag: "div") - .font(size: .sm) - .on { - xs { - padding(of: 1) - } - sm { - padding(of: 2) - } - md { - font(size: .lg) - } - lg { - margins(of: 3) - } - xl { - border(of: 1, color: .blue(._500)) - } - xl2 { - background(color: .gray(._200)) - } - } - - let rendered = element.render() - #expect(rendered.contains("text-sm")) - #expect(rendered.contains("xs:p-1")) - #expect(rendered.contains("sm:p-2")) - #expect(rendered.contains("md:text-lg")) - #expect(rendered.contains("lg:m-3")) - #expect(rendered.contains("xl:border-1")) - #expect(rendered.contains("xl:border-blue-500")) - #expect(rendered.contains("2xl:bg-gray-200")) - } -} diff --git a/Tests/WebUITests/Styles/ResponsiveDSLTests.swift b/Tests/WebUITests/Styles/ResponsiveDSLTests.swift deleted file mode 100644 index 37208c45..00000000 --- a/Tests/WebUITests/Styles/ResponsiveDSLTests.swift +++ /dev/null @@ -1,147 +0,0 @@ -import Testing - -@testable import WebUI - -@Suite("Responsive DSL Tests") struct ResponsiveDSLTests { - // MARK: - Basic Tests - - @Test("Basic font styling with result builder syntax") - func testBasicFontStylingWithDSL() async throws { - let element = Element(tag: "div") - .font(size: .sm) - .on { - md { - font(size: .lg) - } - } - - let rendered = element.render() - #expect(rendered.contains("text-sm")) - #expect(rendered.contains("md:text-lg")) - } - - @Test("Multiple breakpoints with result builder syntax") - func testMultipleBreakpointsWithDSL() async throws { - let element = Element(tag: "div") - .background(color: .gray(._100)) - .font(size: .sm) - .on { - sm { - font(size: .base) - } - md { - font(size: .lg) - background(color: .gray(._200)) - } - lg { - font(size: .xl) - background(color: .gray(._300)) - } - } - - let rendered = element.render() - #expect(rendered.contains("bg-gray-100")) - #expect(rendered.contains("text-sm")) - #expect(rendered.contains("sm:text-base")) - #expect(rendered.contains("md:text-lg")) - #expect(rendered.contains("md:bg-gray-200")) - #expect(rendered.contains("lg:text-xl")) - #expect(rendered.contains("lg:bg-gray-300")) - } - - @Test("Multiple style types with result builder syntax") - func testMultipleStyleTypesWithDSL() async throws { - let element = Element(tag: "div") - .padding(of: 2) - .font(size: .sm) - .on { - md { - padding(of: 4) - font(size: .lg) - margins(of: 2) - } - } - - let rendered = element.render() - #expect(rendered.contains("p-2")) - #expect(rendered.contains("text-sm")) - #expect(rendered.contains("md:p-4")) - #expect(rendered.contains("md:text-lg")) - #expect(rendered.contains("md:m-2")) - } - - // MARK: - Layout Tests - - @Test("Flex layout with result builder syntax") - func testFlexLayoutWithDSL() async throws { - let element = Element(tag: "div") - .flex(direction: .column) - .on { - md { - flex(direction: .row, justify: .between) - } - } - - let rendered = element.render() - #expect(rendered.contains("flex")) - #expect(rendered.contains("flex-col")) - #expect(rendered.contains("md:flex")) - #expect(rendered.contains("md:justify-between")) - } - - @Test("Grid layout with result builder syntax") - func testGridLayoutWithDSL() async throws { - let element = Element(tag: "div") - .grid(columns: 1) - .on { - md { - grid(columns: 2) - } - lg { - grid(columns: 3) - } - } - - let rendered = element.render() - #expect(rendered.contains("grid")) - #expect(rendered.contains("grid-cols-1")) - #expect(rendered.contains("md:grid-cols-2")) - #expect(rendered.contains("lg:grid-cols-3")) - } - - // MARK: - Backward Compatibility - - @Test("Test all breakpoint modifiers") - func testAllBreakpointModifiers() async throws { - let element = Element(tag: "div") - .on { - xs { - padding(of: 1) - } - sm { - font(size: .sm) - } - md { - background(color: .blue(._300)) - } - lg { - margins(of: 4) - } - xl { - border(of: 1, color: .gray(._200)) - } - xl2 { - rounded(.lg) - } - } - - let rendered = element.render() - #expect(rendered.contains("xs:p-1")) - #expect(rendered.contains("sm:text-sm")) - #expect(rendered.contains("md:bg-blue-300")) - #expect(rendered.contains("lg:m-4")) - #expect(rendered.contains("xl:border-1")) - #expect(rendered.contains("xl:border-gray-200")) - #expect(rendered.contains("2xl:rounded-lg")) - } -} diff --git a/Tests/WebUITests/Styles/ResponsiveStyleBuilderTests.swift b/Tests/WebUITests/Styles/ResponsiveStyleBuilderTests.swift deleted file mode 100644 index 5152b64f..00000000 --- a/Tests/WebUITests/Styles/ResponsiveStyleBuilderTests.swift +++ /dev/null @@ -1,219 +0,0 @@ -import Testing - -@testable import WebUI - -@Suite("Responsive Style Builder Tests") struct ResponsiveStyleBuilderTests { - // MARK: - Basic Tests - - @Test("Basic responsive font styling with result builder syntax") - func testBasicResponsiveFontStylingNewSyntax() async throws { - let element = Element(tag: "div") - .font(size: .sm) - .on { - md { - font(size: .lg) - } - } - - let rendered = element.render() - #expect(rendered.contains("class=\"text-sm md:text-lg\"")) - } - - @Test("Multiple breakpoints with result builder syntax") - func testMultipleBreakpointsNewSyntax() async throws { - let element = Element(tag: "div") - .background(color: .gray(._100)) - .font(size: .sm) - .on { - sm { - font(size: .base) - } - md { - font(size: .lg) - background(color: .gray(._200)) - } - lg { - font(size: .xl) - background(color: .gray(._300)) - } - } - - let rendered = element.render() - #expect( - rendered.contains( - "class=\"bg-gray-100 text-sm sm:text-base md:text-lg md:bg-gray-200 lg:text-xl lg:bg-gray-300\"" - ) - ) - } - - // MARK: - Multiple Style Types Tests - - @Test("Multiple style types with result builder syntax") - func testMultipleStyleTypesNewSyntax() async throws { - let element = Element(tag: "div") - .padding(of: 2) - .font(size: .sm) - .on { - md { - padding(of: 4) - font(size: .lg) - margins(of: 2) - } - } - - let rendered = element.render() - #expect(rendered.contains("class=\"p-2 text-sm md:p-4 md:text-lg md:m-2\"")) - } - - // MARK: - Complex Component Tests - - @Test("Complex component with result builder syntax") - func testComplexComponentNewSyntax() async throws { - let button = Button(type: .submit) { "Submit" } - .background(color: .blue(._500)) - .font(color: .blue(._50)) - .padding(of: 2) - .rounded(.md) - .on { - sm { - padding(of: 3) - } - md { - padding(of: 4) - font(size: .lg) - } - lg { - padding(of: 6) - background(color: .blue(._600)) - } - } - - let rendered = button.render() - #expect(rendered.contains("type=\"submit\"")) - #expect(rendered.contains("bg-blue-500")) - #expect(rendered.contains("text-blue-50")) - #expect(rendered.contains("p-2")) - #expect(rendered.contains("rounded-md")) - #expect(rendered.contains("sm:p-3")) - #expect(rendered.contains("md:p-4")) - #expect(rendered.contains("md:text-lg")) - #expect(rendered.contains("lg:p-6")) - #expect(rendered.contains("lg:bg-blue-600")) - } - - // MARK: - Layout Tests - - @Test("Responsive flex layout with result builder syntax") - func testResponsiveFlexLayoutNewSyntax() async throws { - let element = Element(tag: "div") - .flex(direction: .column) - .on { - md { - flex(direction: .row, justify: .between) - } - } - - let rendered = element.render() - #expect(rendered.contains("flex")) - #expect(rendered.contains("flex-col")) - #expect(rendered.contains("md:flex")) - #expect(rendered.contains("md:flex-row")) - #expect(rendered.contains("md:justify-between")) - } - - @Test("Responsive grid layout with result builder syntax") - func testResponsiveGridLayoutNewSyntax() async throws { - let element = Element(tag: "div") - .grid(columns: 1) - .on { - md { - grid(columns: 2) - } - lg { - grid(columns: 3) - } - } - - let rendered = element.render() - #expect(rendered.contains("class=\"grid grid-cols-1 md:grid md:grid-cols-2 lg:grid lg:grid-cols-3\"")) - } - - // MARK: - Visibility Tests - - @Test("Responsive visibility with result builder syntax") - func testResponsiveVisibilityNewSyntax() async throws { - let element = Element(tag: "div") - .hidden() - .responsive { - md { - hidden(false) - } - } - - let rendered = element.render() - #expect(rendered.contains("class=\"hidden\"")) - #expect(!rendered.contains("md:hidden")) - } - - // MARK: - Compound Tests - - @Test("Responsive nav menu with result builder syntax") - func testResponsiveNavMenuNewSyntax() async throws { - let nav = Navigation { - Stack(classes: ["mobile-menu"]) - .responsive { - md { - hidden() - } - } - - List(classes: ["desktop-menu"]) - .hidden() - .responsive { - md { - hidden(false) - } - } - } - - let rendered = nav.render() - #expect(rendered.contains("class=\"mobile-menu md:hidden\"")) - #expect(rendered.contains("class=\"desktop-menu hidden\"")) - } - - // MARK: - Backward Compatibility Test - - @Test("Test all breakpoint modifiers") - func testAllBreakpointModifiers() async throws { - let element = Element(tag: "div") - .responsive { - xs { - padding(of: 1) - } - sm { - padding(of: 2) - } - md { - font(size: .lg) - } - lg { - margins(of: 3) - } - xl { - border(of: 1, color: .blue(._500)) - } - xl2 { - background(color: .gray(._200)) - } - } - - let rendered = element.render() - #expect(rendered.contains("xs:p-1")) - #expect(rendered.contains("sm:p-2")) - #expect(rendered.contains("md:text-lg")) - #expect(rendered.contains("lg:m-3")) - #expect(rendered.contains("xl:border-1")) - #expect(rendered.contains("xl:border-blue-500")) - #expect(rendered.contains("2xl:bg-gray-200")) - } -} diff --git a/Tests/WebUITests/Styles/ResponsiveTests.swift b/Tests/WebUITests/Styles/ResponsiveTests.swift index 5fdecbaa..e85c07aa 100644 --- a/Tests/WebUITests/Styles/ResponsiveTests.swift +++ b/Tests/WebUITests/Styles/ResponsiveTests.swift @@ -2,11 +2,11 @@ import Testing @testable import WebUI -@Suite("Responsive Style Tests") struct ResponsiveStyleTests { +@Suite("New Responsive Style Tests") struct NewResponsiveTests { // MARK: - Basic Responsive Tests - @Test("Basic responsive font styling") - func testBasicResponsiveFontStyling() async throws { + @Test("Basic responsive font styling with result builder syntax") + func testBasicResponsiveFontStylingNewSyntax() async throws { let element = Element(tag: "div") .font(size: .sm) .responsive { @@ -19,12 +19,12 @@ import Testing #expect(rendered.contains("class=\"text-sm md:text-lg\"")) } - @Test("Multiple breakpoints in one responsive block") - func testMultipleBreakpoints() async throws { + @Test("Multiple breakpoints with result builder syntax") + func testMultipleBreakpointsNewSyntax() async throws { let element = Element(tag: "div") .background(color: .gray(._100)) .font(size: .sm) - .responsive { + .on { sm { font(size: .base) } @@ -48,12 +48,12 @@ import Testing // MARK: - Multiple Style Types Tests - @Test("Responsive block with multiple style types") - func testResponsiveWithMultipleStyleTypes() async throws { + @Test("Multiple style types with result builder syntax") + func testMultipleStyleTypesNewSyntax() async throws { let element = Element(tag: "div") .padding(of: 2) .font(size: .sm) - .responsive { + .on { md { padding(of: 4) font(size: .lg) @@ -67,14 +67,14 @@ import Testing // MARK: - Complex Component Tests - @Test("Responsive styling with a complex component") - func testResponsiveWithComplexComponent() async throws { + @Test("Complex component with result builder syntax") + func testComplexComponentNewSyntax() async throws { let button = Button(type: .submit) { "Submit" } .background(color: .blue(._500)) .font(color: .blue(._50)) .padding(of: 2) .rounded(.md) - .responsive { + .on { sm { padding(of: 3) } @@ -103,11 +103,11 @@ import Testing // MARK: - Layout Tests - @Test("Responsive flex layout") - func testResponsiveFlexLayout() async throws { + @Test("Responsive flex layout with result builder syntax") + func testResponsiveFlexLayoutNewSyntax() async throws { let element = Element(tag: "div") .flex(direction: .column) - .responsive { + .on { md { flex(direction: .row, justify: .between) } @@ -117,149 +117,83 @@ import Testing #expect(rendered.contains("flex")) #expect(rendered.contains("flex-col")) #expect(rendered.contains("md:flex")) + #expect(rendered.contains("md:flex-row")) + #expect(rendered.contains("md:justify-between")) } - // MARK: - Sizing & Position Tests - - @Test("Responsive sizing") - func testResponsiveSizing() async throws { + @Test("Responsive grid layout with result builder syntax") + func testResponsiveGridLayoutNewSyntax() async throws { let element = Element(tag: "div") - .frame(width: 100) - .responsive { + .grid(columns: 1) + .on { md { - frame(maxWidth: .spacing(600)) - margins(at: .horizontal, auto: true) + grid(columns: 2) } - } - - let rendered = element.render() - #expect(rendered.contains("class=\"w-100 md:max-w-600 md:mx-auto\"")) - } - - @Test("Responsive positioning") - func testResponsivePositioning() async throws { - let element = Element(tag: "div") - .position(.relative) - .responsive { lg { - position(.fixed, at: .top, offset: 0) - frame(width: .spacing(100)) + grid(columns: 3) } } let rendered = element.render() - #expect(rendered.contains("class=\"relative lg:fixed lg:top-0 lg:w-100\"")) + print("RENDERED:", rendered) + #expect(rendered.contains("grid")) + #expect(rendered.contains("grid-cols-1")) + #expect(rendered.contains("md:grid-cols-2")) + #expect(rendered.contains("lg:grid-cols-3")) } // MARK: - Visibility Tests - @Test("Responsive visibility") - func testResponsiveVisibility() async throws { + @Test("Responsive visibility with result builder syntax") + func testResponsiveVisibilityNewSyntax() async throws { let element = Element(tag: "div") .hidden() - .responsive { + .on { md { hidden(false) } } let rendered = element.render() - // This test would need a special implementation to verify the absence - // of the .md:hidden class or the presence of a display value #expect(rendered.contains("class=\"hidden\"")) #expect(!rendered.contains("md:hidden")) } - // MARK: - Compound Tests - - @Test("Responsive nav menu") - func testResponsiveNavMenu() async throws { - let nav = Navigation { - Stack(classes: ["mobile-menu"]) - .responsive { - md { - hidden() - } - } - - List(classes: ["desktop-menu"]) - .hidden() - .responsive { - md { - hidden(false) - } - } - } - - let rendered = nav.render() - #expect(rendered.contains("class=\"mobile-menu md:hidden\"")) - #expect(rendered.contains("class=\"desktop-menu hidden\"")) - } - - @Test("Responsive hero section") - func testResponsiveHeroSection() async throws { - let hero = Section(classes: ["hero"]) - .padding(of: 4) - .responsive { - sm { - padding(of: 6) - } - md { - padding(of: 8) - } - lg { - padding(of: 12) - frame(height: .spacing(100)) - } - } - - let rendered = hero.render() - #expect(rendered.contains("class=\"hero p-4 sm:p-6 md:p-8 lg:p-12 lg:h-100\"")) - } + // MARK: - Backward Compatibility Test - // MARK: - Testing All Breakpoint Modifiers - - @Test("All breakpoint modifiers with different styles") + @Test("Test all breakpoint modifiers") func testAllBreakpointModifiers() async throws { let element = Element(tag: "div") - .responsive { + .font(size: .sm) + .on { xs { padding(of: 1) } sm { padding(of: 2) - font(size: .sm) } md { - margins(of: 3) - background(color: .blue(._100)) + font(size: .lg) } lg { - font(size: .lg) - border(of: 1, color: .gray(._300)) + margins(of: 3) } xl { - padding(of: 4) - position(.relative) + border(of: 1, color: .blue(._500)) } xl2 { - margins(of: 5) - rounded(.lg) + background(color: .gray(._200)) } } let rendered = element.render() + #expect(rendered.contains("text-sm")) #expect(rendered.contains("xs:p-1")) #expect(rendered.contains("sm:p-2")) - #expect(rendered.contains("sm:text-sm")) - #expect(rendered.contains("md:m-3")) - #expect(rendered.contains("md:bg-blue-100")) - #expect(rendered.contains("lg:text-lg")) - #expect(rendered.contains("lg:border-1")) - #expect(rendered.contains("lg:border-gray-300")) - #expect(rendered.contains("xl:p-4")) - #expect(rendered.contains("xl:relative")) - #expect(rendered.contains("2xl:m-5")) - #expect(rendered.contains("2xl:rounded-lg")) + #expect(rendered.contains("md:text-lg")) + #expect(rendered.contains("lg:m-3")) + #expect(rendered.contains("xl:border-1")) + #expect(rendered.contains("xl:border-blue-500")) + #expect(rendered.contains("2xl:bg-gray-200")) } } diff --git a/examples/InteractionModifiersExample.swift b/examples/InteractionModifiersExample.swift index ca73d35f..cf2106cd 100644 --- a/examples/InteractionModifiersExample.swift +++ b/examples/InteractionModifiersExample.swift @@ -9,7 +9,7 @@ struct InteractiveButton: HTML { var isPrimary: Bool = true var isDisabled: Bool = false var onClick: String? = nil - + func render() -> String { Button(disabled: isDisabled, onClick: onClick) { label } // Base styles @@ -17,7 +17,7 @@ struct InteractiveButton: HTML { .rounded(.md) .transition(of: .all, for: 150) .font(weight: .medium) - + // Conditional primary/secondary styles .on { if isPrimary { @@ -29,7 +29,7 @@ struct InteractiveButton: HTML { border(of: 1, color: .gray(._300)) } } - + // Interactive state modifiers .on { // Hover state @@ -41,7 +41,7 @@ struct InteractiveButton: HTML { } transform(scale: (x: 102, y: 102)) } - + // Focus state (accessibility) focus { if isPrimary { @@ -52,7 +52,7 @@ struct InteractiveButton: HTML { outline(style: .solid) transform(translateY: -1) } - + // Active state (when pressing) active { if isPrimary { @@ -62,7 +62,7 @@ struct InteractiveButton: HTML { } transform(scale: (x: 98, y: 98)) } - + // Disabled state disabled { if isPrimary { @@ -90,7 +90,7 @@ struct FormInput: HTML { var isRequired: Bool = false var isInvalid: Bool = false var value: String? = nil - + func render() -> String { Div { Label(for: id) { label } @@ -104,7 +104,7 @@ struct FormInput: HTML { } } } - + Input(id: id, value: value, placeholder: placeholder, required: isRequired) .padding(of: 3) .rounded(.md) @@ -112,7 +112,7 @@ struct FormInput: HTML { .width(.full) .font(size: .sm) .transition(of: .all, for: 150) - + // Interactive state styling .on { // Placeholder styling @@ -120,24 +120,24 @@ struct FormInput: HTML { font(color: .gray(._400)) font(weight: .light) } - + // Focus state focus { border(of: 1, color: .blue(._500)) shadow(of: .sm, color: .blue(._100)) } - + // When the field is invalid if isInvalid { border(of: 1, color: .red(._500)) - + // Invalid + focus state focus { border(of: 1, color: .red(._500)) shadow(of: .sm, color: .red(._100)) } } - + // ARIA required state ariaRequired { border(of: 1, style: .solid) @@ -156,14 +156,14 @@ struct NavMenuItem: HTML { var label: String var href: String var isSelected: Bool = false - + func render() -> String { Link(to: href) { label } .padding(vertical: 2, horizontal: 4) .rounded(.md) .font(size: .sm) .transition(of: .all, for: 150) - + // Base state .on { if isSelected { @@ -175,7 +175,7 @@ struct NavMenuItem: HTML { font(color: .gray(._700)) } } - + // Interactive states .on { // Hover state @@ -186,14 +186,14 @@ struct NavMenuItem: HTML { background(color: .blue(._100)) } } - + // Focus state for keyboard navigation focus { outline(of: 2, color: .blue(._300)) outline(style: .solid) outline(offset: 1) } - + // Active state (when pressing) active { if !isSelected { @@ -203,7 +203,7 @@ struct NavMenuItem: HTML { } transform(scale: (x: 98, y: 98)) } - + // ARIA selected state for screen readers ariaSelected { font(weight: .semibold) @@ -216,18 +216,18 @@ struct NavMenuItem: HTML { /// Example usage in a page context struct InteractiveComponentsDemo: HTML { func render() -> String { - return Document(title: "Interactive Components Demo") { + Document(title: "Interactive Components Demo") { Section { Heading(level: 1) { "Interactive Components Demo" } .font(size: .xl2) .padding(bottom: 6) - + // Buttons section Div { Heading(level: 2) { "Buttons" } .font(size: .xl) .padding(bottom: 4) - + Div { InteractiveButton(label: "Primary Button") InteractiveButton(label: "Secondary Button", isPrimary: false) @@ -237,28 +237,33 @@ struct InteractiveComponentsDemo: HTML { .display(.flex) } .padding(bottom: 8) - + // Form section Div { Heading(level: 2) { "Form Inputs" } .font(size: .xl) .padding(bottom: 4) - + Div { FormInput(id: "name", label: "Name", placeholder: "Enter your name") FormInput(id: "email", label: "Email", placeholder: "Enter your email", isRequired: true) - FormInput(id: "password", label: "Password", placeholder: "Enter your password", isInvalid: true) + FormInput( + id: "password", + label: "Password", + placeholder: "Enter your password", + isInvalid: true + ) } .spacing(of: 4, along: .vertical) } .padding(bottom: 8) - + // Navigation section Div { Heading(level: 2) { "Navigation" } .font(size: .xl) .padding(bottom: 4) - + Nav { Div { NavMenuItem(label: "Home", href: "/", isSelected: true) @@ -277,4 +282,4 @@ struct InteractiveComponentsDemo: HTML { } .render() } -} \ No newline at end of file +} From 0d8bd3495b670834c0af3545a8056dd501f254a1 Mon Sep 17 00:00:00 2001 From: Mac Long Date: Fri, 23 May 2025 10:32:07 +0100 Subject: [PATCH 2/5] Update CONTRIBUTING to match new patterns better --- CONTRIBUTING.md | 47 ++++++++++++++++++++--------------------------- 1 file changed, 20 insertions(+), 27 deletions(-) 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: From bb1273b29f4ded8cbd2753e5e1959dcf4b998d9e Mon Sep 17 00:00:00 2001 From: Mac Date: Fri, 23 May 2025 12:09:03 +0100 Subject: [PATCH 3/5] Move Core, Theme, and Infrastructure directories into new structure Move directories into new organized structure --- .../Metadata/Metadata.swift | 0 .../Metadata/MetadataComponents.swift | 0 .../Metadata/MetadataTypes.swift | 0 .../StructuredData/ArticleSchema.swift | 0 .../StructuredData/FaqSchema.swift | 0 .../StructuredData/OrganizationSchema.swift | 0 .../StructuredData/PersonSchema.swift | 0 .../StructuredData/ProductSchema.swift | 0 .../StructuredData/SchemaType.swift | 0 .../StructuredData/StructuredData.swift | 0 .../Core/{ => Infrastructure}/Website.swift | 0 Sources/WebUI/Core/SEO.swift | 250 ------------ Sources/WebUI/Core/{ => Theme}/Theme.swift | 0 .../WebUI/Elements/Base/Abbreviation.swift | 18 +- Sources/WebUI/Elements/Base/Emphasis.swift | 43 ++ Sources/WebUI/Elements/Base/Link.swift | 63 +++ .../WebUI/Elements/Base/Preformatted.swift | 26 ++ Sources/WebUI/Elements/Base/Strong.swift | 26 ++ Sources/WebUI/Elements/Base/Text.swift | 3 + Sources/WebUI/Elements/Base/Time.swift | 36 ++ Sources/WebUI/Elements/Form/Input.swift | 19 - .../Interactive/Form/Input/InputType.swift | 18 + .../WebUI/Elements/Media/Audio/Audio.swift | 83 ++++ .../Elements/Media/Audio/AudioType.swift | 21 + .../WebUI/Elements/Media/Image/Figure.swift | 84 ++++ .../WebUI/Elements/Media/Image/Image.swift | 58 +++ .../Elements/Media/Image/ImageType.swift | 21 + .../WebUI/Elements/Media/Image/Picture.swift | 65 +++ Sources/WebUI/Elements/Media/MediaSize.swift | 29 ++ .../WebUI/Elements/Media/Video/Video.swift | 94 +++++ .../Elements/Media/Video/VideoType.swift | 22 ++ Sources/WebUI/Elements/Progress.swift | 60 +++ .../WebUI/Elements/Structure/Fragment.swift | 43 ++ .../Elements/Structure/Layout/Article.swift | 51 +++ .../Elements/Structure/Layout/Aside.swift | 54 +++ .../Elements/Structure/Layout/Header.swift | 52 +++ .../Elements/Structure/Layout/Main.swift | 56 +++ .../Structure/Layout/Navigation.swift | 53 +++ .../WebUI/Elements/Structure/List/Item.swift | 50 +++ .../Elements/Structure/List/ListStyle.swift | 17 + .../Elements/Structure/List/ListType.swift | 15 + Sources/WebUI/Elements/Structure/Stack.swift | 58 +++ Sources/WebUI/Elements/Text/Code.swift | 42 ++ .../WebUI/Elements/Text/Heading/Heading.swift | 45 +++ .../Elements/Text/Heading/HeadingLevel.swift | 27 ++ .../Color/BackgroundStyleOperation.swift | 107 +++++ .../Color/Border/BorderStyleOperation.swift | 213 ++++++++++ .../Styles/Color/Border/BorderTypes.swift | 85 ++++ .../Styles/Color/RingStyleOperation.swift | 160 ++++++++ .../Styles/{Base => Core}/Utilities.swift | 10 + .../Effects/BorderRadiusStyleOperation.swift | 143 +++++++ .../Effects/OpacityStyleOperation.swift | 98 +++++ .../Effects/OutlineStyleOperation.swift | 188 +++++++++ .../Effects/Shadow/ShadowStyleOperation.swift | 155 ++++++++ .../Styles/Effects/Shadow/ShadowTypes.swift | 27 ++ .../TransformStyleOperation.swift | 0 .../Transition/TransitionStyleOperation.swift | 0 .../Transition/TransitionTypes.swift | 0 .../CursorStyleOperation.swift | 0 .../Interactivity/InteractionModifiers.swift | 370 ++++++++++++++++++ .../Display/DisplayStyleOperation.swift | 103 +++++ .../Styles/Layout/Display/DisplayTypes.swift | 88 +++++ .../Layout/Display/FlexStyleOperation.swift | 259 ++++++++++++ .../Layout/Display/GridStyleOperation.swift | 219 +++++++++++ .../Display/VisibilityStyleOperation.swift | 110 ++++++ .../MarginsStyleOperation.swift | 0 .../Overflow/OverflowStyleOperation.swift | 0 .../Overflow/OverflowTypes.swift | 0 .../PaddingStyleOperation.swift | 0 .../Position/PositionStyleOperation.swift | 0 .../Position/PositionTypes.swift | 0 .../Scroll/ScrollStyleOperation.swift | 0 .../Scroll/ScrollTypes.swift | 0 .../Sizing/SizingStyleOperation.swift | 0 .../{Base => Layout}/Sizing/SizingTypes.swift | 0 .../SpacingStyleOperation.swift | 0 .../ZIndexStyleOperation.swift | 0 .../Styles/Responsive/ResponsiveAlias.swift | 15 + .../Responsive/ResponsiveModifier.swift | 225 +++++++++++ .../Responsive/ResponsiveStyleBuilder.swift | 75 ++++ .../Responsive/ResponsiveStyleModifiers.swift | 214 ++++++++++ .../Font/FontStyleOperation.swift | 0 .../{Base => Typography}/Font/FontTypes.swift | 0 83 files changed, 4188 insertions(+), 278 deletions(-) rename Sources/WebUI/Core/{ => Infrastructure}/Metadata/Metadata.swift (100%) rename Sources/WebUI/Core/{ => Infrastructure}/Metadata/MetadataComponents.swift (100%) rename Sources/WebUI/Core/{ => Infrastructure}/Metadata/MetadataTypes.swift (100%) rename Sources/WebUI/Core/{ => Infrastructure}/StructuredData/ArticleSchema.swift (100%) rename Sources/WebUI/Core/{ => Infrastructure}/StructuredData/FaqSchema.swift (100%) rename Sources/WebUI/Core/{ => Infrastructure}/StructuredData/OrganizationSchema.swift (100%) rename Sources/WebUI/Core/{ => Infrastructure}/StructuredData/PersonSchema.swift (100%) rename Sources/WebUI/Core/{ => Infrastructure}/StructuredData/ProductSchema.swift (100%) rename Sources/WebUI/Core/{ => Infrastructure}/StructuredData/SchemaType.swift (100%) rename Sources/WebUI/Core/{ => Infrastructure}/StructuredData/StructuredData.swift (100%) rename Sources/WebUI/Core/{ => Infrastructure}/Website.swift (100%) delete mode 100644 Sources/WebUI/Core/SEO.swift rename Sources/WebUI/Core/{ => Theme}/Theme.swift (100%) create mode 100644 Sources/WebUI/Elements/Base/Emphasis.swift create mode 100644 Sources/WebUI/Elements/Base/Link.swift create mode 100644 Sources/WebUI/Elements/Base/Preformatted.swift create mode 100644 Sources/WebUI/Elements/Base/Strong.swift create mode 100644 Sources/WebUI/Elements/Base/Time.swift create mode 100644 Sources/WebUI/Elements/Interactive/Form/Input/InputType.swift create mode 100644 Sources/WebUI/Elements/Media/Audio/Audio.swift create mode 100644 Sources/WebUI/Elements/Media/Audio/AudioType.swift create mode 100644 Sources/WebUI/Elements/Media/Image/Figure.swift create mode 100644 Sources/WebUI/Elements/Media/Image/Image.swift create mode 100644 Sources/WebUI/Elements/Media/Image/ImageType.swift create mode 100644 Sources/WebUI/Elements/Media/Image/Picture.swift create mode 100644 Sources/WebUI/Elements/Media/MediaSize.swift create mode 100644 Sources/WebUI/Elements/Media/Video/Video.swift create mode 100644 Sources/WebUI/Elements/Media/Video/VideoType.swift create mode 100644 Sources/WebUI/Elements/Progress.swift create mode 100644 Sources/WebUI/Elements/Structure/Fragment.swift create mode 100644 Sources/WebUI/Elements/Structure/Layout/Article.swift create mode 100644 Sources/WebUI/Elements/Structure/Layout/Aside.swift create mode 100644 Sources/WebUI/Elements/Structure/Layout/Header.swift create mode 100644 Sources/WebUI/Elements/Structure/Layout/Main.swift create mode 100644 Sources/WebUI/Elements/Structure/Layout/Navigation.swift create mode 100644 Sources/WebUI/Elements/Structure/List/Item.swift create mode 100644 Sources/WebUI/Elements/Structure/List/ListStyle.swift create mode 100644 Sources/WebUI/Elements/Structure/List/ListType.swift create mode 100644 Sources/WebUI/Elements/Structure/Stack.swift create mode 100644 Sources/WebUI/Elements/Text/Code.swift create mode 100644 Sources/WebUI/Elements/Text/Heading/Heading.swift create mode 100644 Sources/WebUI/Elements/Text/Heading/HeadingLevel.swift create mode 100644 Sources/WebUI/Styles/Color/BackgroundStyleOperation.swift create mode 100644 Sources/WebUI/Styles/Color/Border/BorderStyleOperation.swift create mode 100644 Sources/WebUI/Styles/Color/Border/BorderTypes.swift create mode 100644 Sources/WebUI/Styles/Color/RingStyleOperation.swift rename Sources/WebUI/Styles/{Base => Core}/Utilities.swift (98%) create mode 100644 Sources/WebUI/Styles/Effects/BorderRadiusStyleOperation.swift create mode 100644 Sources/WebUI/Styles/Effects/OpacityStyleOperation.swift create mode 100644 Sources/WebUI/Styles/Effects/OutlineStyleOperation.swift create mode 100644 Sources/WebUI/Styles/Effects/Shadow/ShadowStyleOperation.swift create mode 100644 Sources/WebUI/Styles/Effects/Shadow/ShadowTypes.swift rename Sources/WebUI/Styles/{Positioning => Effects}/TransformStyleOperation.swift (100%) rename Sources/WebUI/Styles/{Positioning => Effects}/Transition/TransitionStyleOperation.swift (100%) rename Sources/WebUI/Styles/{Positioning => Effects}/Transition/TransitionTypes.swift (100%) rename Sources/WebUI/Styles/{Base => Interactivity}/CursorStyleOperation.swift (100%) create mode 100644 Sources/WebUI/Styles/Interactivity/InteractionModifiers.swift create mode 100644 Sources/WebUI/Styles/Layout/Display/DisplayStyleOperation.swift create mode 100644 Sources/WebUI/Styles/Layout/Display/DisplayTypes.swift create mode 100644 Sources/WebUI/Styles/Layout/Display/FlexStyleOperation.swift create mode 100644 Sources/WebUI/Styles/Layout/Display/GridStyleOperation.swift create mode 100644 Sources/WebUI/Styles/Layout/Display/VisibilityStyleOperation.swift rename Sources/WebUI/Styles/{Base => Layout}/MarginsStyleOperation.swift (100%) rename Sources/WebUI/Styles/{Positioning => Layout}/Overflow/OverflowStyleOperation.swift (100%) rename Sources/WebUI/Styles/{Positioning => Layout}/Overflow/OverflowTypes.swift (100%) rename Sources/WebUI/Styles/{Base => Layout}/PaddingStyleOperation.swift (100%) rename Sources/WebUI/Styles/{Positioning => Layout}/Position/PositionStyleOperation.swift (100%) rename Sources/WebUI/Styles/{Positioning => Layout}/Position/PositionTypes.swift (100%) rename Sources/WebUI/Styles/{Positioning => Layout}/Scroll/ScrollStyleOperation.swift (100%) rename Sources/WebUI/Styles/{Positioning => Layout}/Scroll/ScrollTypes.swift (100%) rename Sources/WebUI/Styles/{Base => Layout}/Sizing/SizingStyleOperation.swift (100%) rename Sources/WebUI/Styles/{Base => Layout}/Sizing/SizingTypes.swift (100%) rename Sources/WebUI/Styles/{Base => Layout}/SpacingStyleOperation.swift (100%) rename Sources/WebUI/Styles/{Positioning => Layout}/ZIndexStyleOperation.swift (100%) create mode 100644 Sources/WebUI/Styles/Responsive/ResponsiveAlias.swift create mode 100644 Sources/WebUI/Styles/Responsive/ResponsiveModifier.swift create mode 100644 Sources/WebUI/Styles/Responsive/ResponsiveStyleBuilder.swift create mode 100644 Sources/WebUI/Styles/Responsive/ResponsiveStyleModifiers.swift rename Sources/WebUI/Styles/{Base => Typography}/Font/FontStyleOperation.swift (100%) rename Sources/WebUI/Styles/{Base => Typography}/Font/FontTypes.swift (100%) diff --git a/Sources/WebUI/Core/Metadata/Metadata.swift b/Sources/WebUI/Core/Infrastructure/Metadata/Metadata.swift similarity index 100% rename from Sources/WebUI/Core/Metadata/Metadata.swift rename to Sources/WebUI/Core/Infrastructure/Metadata/Metadata.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/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/Elements/Base/Abbreviation.swift b/Sources/WebUI/Elements/Base/Abbreviation.swift index b8879c86..1379feca 100644 --- a/Sources/WebUI/Elements/Base/Abbreviation.swift +++ b/Sources/WebUI/Elements/Base/Abbreviation.swift @@ -1,11 +1,11 @@ import Foundation -/// Generates an HTML abbreviation element (``) for displaying abbreviations or acronyms. +/// Creates HTML abbreviation elements for displaying abbreviations or acronyms. /// -/// The abbreviation element allows you to define the full term for an abbreviation or acronym, -/// which helps with accessibility and provides a visual indication to users (typically shown -/// with a dotted underline). When users hover over the abbreviation, browsers typically -/// display the full term as a tooltip. +/// Represents the full form of an abbreviation or acronym, enhancing accessibility and usability. +/// Abbreviation elements provide a visual indication to users (typically shown with a dotted underline) +/// and display the full term as a tooltip when users hover over them. This improves content comprehension +/// and assists screen reader users. /// /// ## Example /// ```swift @@ -19,11 +19,11 @@ public final class Abbreviation: Element { /// /// - Parameters: /// - title: The full term or explanation of the abbreviation. - /// - id: Unique identifier for the HTML element. + /// - id: Unique identifier for the HTML element, useful for JavaScript interaction and styling. /// - classes: An array of CSS classnames for styling the abbreviation. - /// - 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 element-specific data. + /// - 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 abbreviation. /// - content: Closure providing the content (typically the abbreviated term). /// /// ## Example diff --git a/Sources/WebUI/Elements/Base/Emphasis.swift b/Sources/WebUI/Elements/Base/Emphasis.swift new file mode 100644 index 00000000..0e490270 --- /dev/null +++ b/Sources/WebUI/Elements/Base/Emphasis.swift @@ -0,0 +1,43 @@ +import Foundation + +/// Creates HTML emphasis elements for highlighting important text. +/// +/// Represents emphasized text with semantic importance, typically displayed in italics. +/// Emphasis elements are used to draw attention to text within another body of text +/// and provide semantic meaning that enhances accessibility and comprehension. +public final class Emphasis: Element { + /// Creates a new HTML emphasis element. + /// + /// - Parameters: + /// - id: Unique identifier for the HTML element, useful for JavaScript interaction and styling. + /// - classes: An array of CSS classnames for styling the emphasized text. + /// - 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 element. + /// - content: Closure providing the text content to be emphasized. + /// + /// ## Example + /// ```swift + /// Emphasis { + /// "This text is emphasized" + /// } + /// ``` + 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 + ) + } +} diff --git a/Sources/WebUI/Elements/Base/Link.swift b/Sources/WebUI/Elements/Base/Link.swift new file mode 100644 index 00000000..42a264bc --- /dev/null +++ b/Sources/WebUI/Elements/Base/Link.swift @@ -0,0 +1,63 @@ +import Foundation + +/// Creates HTML anchor elements for linking to other locations. +/// +/// Represents a hyperlink that connects to another web page, file, location within the same page, +/// email address, or any other URL. Links are fundamental interactive elements that enable +/// navigation throughout a website and to external resources. +public final class Link: Element { + private let href: String + private let newTab: Bool? + + /// Creates a new HTML 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, useful for JavaScript interaction and styling. + /// - classes: An array of CSS classnames for styling the link. + /// - role: ARIA role of the element for accessibility, enhancing screen reader interpretation. + /// - label: ARIA label to describe the element for accessibility when link text isn't sufficient. + /// - data: Dictionary of `data-*` attributes for storing custom data relevant to the link. + /// - content: Closure providing link content (text or other HTML elements). + /// + /// ## Example + /// ```swift + /// Link(to: "https://example.com", newTab: true) { + /// "Visit Example Website" + /// } + /// ``` + 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 + ) + } +} diff --git a/Sources/WebUI/Elements/Base/Preformatted.swift b/Sources/WebUI/Elements/Base/Preformatted.swift new file mode 100644 index 00000000..7f2666b9 --- /dev/null +++ b/Sources/WebUI/Elements/Base/Preformatted.swift @@ -0,0 +1,26 @@ +import Foundation + +/// 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. + 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/Strong.swift b/Sources/WebUI/Elements/Base/Strong.swift new file mode 100644 index 00000000..41ea50ef --- /dev/null +++ b/Sources/WebUI/Elements/Base/Strong.swift @@ -0,0 +1,26 @@ +import Foundation + +/// 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. + 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 + ) + } +} diff --git a/Sources/WebUI/Elements/Base/Text.swift b/Sources/WebUI/Elements/Base/Text.swift index 98bb0d72..db692999 100644 --- a/Sources/WebUI/Elements/Base/Text.swift +++ b/Sources/WebUI/Elements/Base/Text.swift @@ -42,6 +42,7 @@ public final class Text: Element { ) } } +<<<<<<< Updated upstream:Sources/WebUI/Elements/Base/Text.swift /// Defines levels for HTML heading tags from h1 to h6. public enum HeadingLevel: String { @@ -322,3 +323,5 @@ public final class Preformatted: Element { ) } } +======= +>>>>>>> Stashed changes:Sources/WebUI/Elements/Text/Text.swift diff --git a/Sources/WebUI/Elements/Base/Time.swift b/Sources/WebUI/Elements/Base/Time.swift new file mode 100644 index 00000000..ac9c57e9 --- /dev/null +++ b/Sources/WebUI/Elements/Base/Time.swift @@ -0,0 +1,36 @@ +import Foundation + +/// 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. + 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 + ) + } +} diff --git a/Sources/WebUI/Elements/Form/Input.swift b/Sources/WebUI/Elements/Form/Input.swift index ea7421d3..2de3484e 100644 --- a/Sources/WebUI/Elements/Form/Input.swift +++ b/Sources/WebUI/Elements/Form/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/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..fa3a3438 --- /dev/null +++ b/Sources/WebUI/Elements/Media/Image/Picture.swift @@ -0,0 +1,65 @@ +import Foundation + +/// Creates HTML picture elements for responsive images. +/// +/// Represents a container for multiple image sources, allowing browsers to choose the most appropriate +/// image format and resolution based on device capabilities. Picture elements enhance website performance +/// by optimizing image delivery based on screen size, resolution, and supported formats. +/// +/// ## Example +/// ```swift +/// Picture( +/// sources: [ +/// (src: "image.webp", type: .webp), +/// (src: "image.jpg", type: .jpeg) +/// ], +/// description: "A responsive image", +/// fallback: "fallback.jpg" +/// ) +public final class Picture: Element { + /// Creates a new HTML picture element. + /// + /// - Parameters: + /// - sources: Array of tuples containing source URL and image MIME type. + /// - description: The alt text for the image for accessibility and SEO. + /// - fallback: Fallback image source URL, 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 picture container. + /// - 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 picture element. + public init( + sources: [(src: String, type: ImageType)], + description: String, + fallback: String? = nil, + size: MediaSize? = nil, + id: String? = nil, + classes: [String]? = nil, + role: AriaRole? = nil, + label: String? = nil, + data: [String: String]? = nil + ) { + let sourceElements = sources.map { (src, type) in + "" + }.joined() + let imgTag: String + if let fallback = fallback { + imgTag = "\"\(description)\"" + } else { + imgTag = "\"\(description)\"" + } + let content: () -> [any HTML] = { + [RawHTML(sourceElements + imgTag)] + } + super.init( + tag: "picture", + id: id, + classes: classes, + role: role, + label: label, + data: data, + content: content + ) + } +} diff --git a/Sources/WebUI/Elements/Media/MediaSize.swift b/Sources/WebUI/Elements/Media/MediaSize.swift new file mode 100644 index 00000000..38ba5f78 --- /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/Video/Video.swift b/Sources/WebUI/Elements/Media/Video/Video.swift new file mode 100644 index 00000000..039c1587 --- /dev/null +++ b/Sources/WebUI/Elements/Media/Video/Video.swift @@ -0,0 +1,94 @@ +import Foundation + +/// Creates HTML video elements for displaying video content. +/// +/// Represents a video player that supports multiple source formats for cross-browser compatibility. +/// Video elements are fundamental for embedding video content such as tutorials, presentations, or promotional material. +/// +/// ## 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 player. + /// + /// - 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, useful for JavaScript interaction and styling. + /// - classes: An array of CSS classnames for styling the video 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 video player. + /// + /// ## 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." + } + ) + } +} diff --git a/Sources/WebUI/Elements/Media/Video/VideoType.swift b/Sources/WebUI/Elements/Media/Video/VideoType.swift new file mode 100644 index 00000000..88e05248 --- /dev/null +++ b/Sources/WebUI/Elements/Media/Video/VideoType.swift @@ -0,0 +1,22 @@ +import Foundation + +/// Defines video MIME types for use with video elements. +/// +/// Used to specify the format of video files, ensuring browsers can properly interpret and play the content. +/// Different browsers support different video formats, so providing multiple source types improves compatibility. +/// +/// ## Example +/// ```swift +/// Video( +/// sources: [ +/// (src: "movie.mp4", type: .mp4), +/// (src: "movie.webm", type: .webm) +/// ], +/// controls: true +/// ) +/// ``` +public enum VideoType: String { + case mp4 = "video/mp4" + case webm = "video/webm" + case ogg = "video/ogg" +} diff --git a/Sources/WebUI/Elements/Progress.swift b/Sources/WebUI/Elements/Progress.swift new file mode 100644 index 00000000..fb63b65c --- /dev/null +++ b/Sources/WebUI/Elements/Progress.swift @@ -0,0 +1,60 @@ +/// Generates an HTML progress element to display task completion status. +/// +/// The progress element visually represents the completion state of a task or process, +/// such as a file download, form submission, or data processing operation. It provides +/// users with visual feedback about ongoing operations. +/// +/// ## Example +/// ```swift +/// Progress(value: 75, max: 100) +/// // Renders: +/// ``` +public final class Progress: Element { + let value: Double? + let max: Double? + + /// Creates a new HTML progress element. + /// + /// - Parameters: + /// - value: Current progress value between 0 and max, optional. When omitted, the progress bar shows an indeterminate state. + /// - max: Maximum progress value (100% completion point), optional. Defaults to 100 when omitted. + /// - id: Unique identifier for the HTML element. + /// - classes: An array of CSS classnames for styling the progress bar. + /// - role: ARIA role of the element for accessibility. + /// - label: ARIA label to describe the element for screen readers (e.g., "Download progress"). + /// - data: Dictionary of `data-*` attributes for storing element-relevant data. + /// + /// ## Example + /// ```swift + /// // Determinate progress bar showing 30% completion + /// Progress(value: 30, max: 100, id: "download-progress", label: "Download progress") + /// + /// // Indeterminate progress bar (activity indicator) + /// Progress(id: "loading-indicator", label: "Loading content") + /// ``` + public init( + value: Double? = nil, + max: Double? = nil, + id: String? = nil, + classes: [String]? = nil, + role: AriaRole? = nil, + label: String? = nil, + data: [String: String]? = nil + ) { + self.value = value + self.max = max + let customAttributes = [ + Attribute.string("value", value?.description), + Attribute.string("max", max?.description), + ].compactMap { $0 } + super.init( + tag: "progress", + id: id, + classes: classes, + role: role, + label: label, + data: data, + customAttributes: customAttributes.isEmpty ? nil : customAttributes + ) + } +} diff --git a/Sources/WebUI/Elements/Structure/Fragment.swift b/Sources/WebUI/Elements/Structure/Fragment.swift new file mode 100644 index 00000000..c1d01bd5 --- /dev/null +++ b/Sources/WebUI/Elements/Structure/Fragment.swift @@ -0,0 +1,43 @@ +/// Generates a generic HTML fragment without a containing element. +/// +/// Groups arbitrary elements together without rendering a parent tag. +/// Unlike other elements that produce an HTML tag, `Fragment` only renders its children. +/// +/// Use `Fragment` for: +/// - Rendering components that have no obvious root tag +/// - Conditional rendering of multiple elements +/// - Returning multiple elements from a component +/// - Avoiding unnecessary DOM nesting +/// +/// - Note: Conceptually similar to React's Fragment or Swift UI's Group component. +public final class Fragment: HTML { + let contentBuilder: () -> [any HTML]? + + /// Computed inner HTML content. + var content: [any HTML] { + contentBuilder() ?? { [] }() + } + + /// Creates a new HTML fragment that renders only its children. + /// + /// - Parameter content: Closure providing fragment content, defaults to empty. + /// + /// ## Example + /// ```swift + /// Fragment { + /// Heading(.largeTitle) { "Title" } + /// Text { "First paragraph" } + /// Text { "Second paragraph" } + /// } + /// // Renders:

Title

First paragraph

Second paragraph

+ /// ``` + public init( + @HTMLBuilder content: @escaping () -> [any HTML] = { [] } + ) { + self.contentBuilder = content + } + + public func render() -> String { + content.map { $0.render() }.joined() + } +} diff --git a/Sources/WebUI/Elements/Structure/Layout/Article.swift b/Sources/WebUI/Elements/Structure/Layout/Article.swift new file mode 100644 index 00000000..1776cc40 --- /dev/null +++ b/Sources/WebUI/Elements/Structure/Layout/Article.swift @@ -0,0 +1,51 @@ +/// 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 + ) + } +} diff --git a/Sources/WebUI/Elements/Structure/Layout/Aside.swift b/Sources/WebUI/Elements/Structure/Layout/Aside.swift new file mode 100644 index 00000000..0423eb06 --- /dev/null +++ b/Sources/WebUI/Elements/Structure/Layout/Aside.swift @@ -0,0 +1,54 @@ +/// 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 + ) + } +} diff --git a/Sources/WebUI/Elements/Structure/Layout/Header.swift b/Sources/WebUI/Elements/Structure/Layout/Header.swift new file mode 100644 index 00000000..67657358 --- /dev/null +++ b/Sources/WebUI/Elements/Structure/Layout/Header.swift @@ -0,0 +1,52 @@ +/// 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 + ) + } +} diff --git a/Sources/WebUI/Elements/Structure/Layout/Main.swift b/Sources/WebUI/Elements/Structure/Layout/Main.swift new file mode 100644 index 00000000..e38a0e98 --- /dev/null +++ b/Sources/WebUI/Elements/Structure/Layout/Main.swift @@ -0,0 +1,56 @@ +/// 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 + ) + } +} diff --git a/Sources/WebUI/Elements/Structure/Layout/Navigation.swift b/Sources/WebUI/Elements/Structure/Layout/Navigation.swift new file mode 100644 index 00000000..8dd8c18b --- /dev/null +++ b/Sources/WebUI/Elements/Structure/Layout/Navigation.swift @@ -0,0 +1,53 @@ +/// 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 + ) + } +} diff --git a/Sources/WebUI/Elements/Structure/List/Item.swift b/Sources/WebUI/Elements/Structure/List/Item.swift new file mode 100644 index 00000000..fa46c8f1 --- /dev/null +++ b/Sources/WebUI/Elements/Structure/List/Item.swift @@ -0,0 +1,50 @@ +/// Generates an HTML list item element (`
  • `). +/// +/// `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:
  • This is a list item with bold text
  • +/// ``` +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:
  • Complete documentation
  • + /// ``` + 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/Structure/List/ListStyle.swift b/Sources/WebUI/Elements/Structure/List/ListStyle.swift new file mode 100644 index 00000000..13e93b83 --- /dev/null +++ b/Sources/WebUI/Elements/Structure/List/ListStyle.swift @@ -0,0 +1,17 @@ +/// 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]" +} diff --git a/Sources/WebUI/Elements/Structure/List/ListType.swift b/Sources/WebUI/Elements/Structure/List/ListType.swift new file mode 100644 index 00000000..ffae93e7 --- /dev/null +++ b/Sources/WebUI/Elements/Structure/List/ListType.swift @@ -0,0 +1,15 @@ +/// 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" +} diff --git a/Sources/WebUI/Elements/Structure/Stack.swift b/Sources/WebUI/Elements/Structure/Stack.swift new file mode 100644 index 00000000..3fb5819e --- /dev/null +++ b/Sources/WebUI/Elements/Structure/Stack.swift @@ -0,0 +1,58 @@ +/// 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. +/// +/// - Note: Utilize the `flex` and `grid` modifiers for layout controls. This is different to +/// SwiftUI's `HStack` and `VStack` because responsive layouts in CSS require a different approach. +/// +/// ## 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/Text/Code.swift b/Sources/WebUI/Elements/Text/Code.swift new file mode 100644 index 00000000..7e31970b --- /dev/null +++ b/Sources/WebUI/Elements/Text/Code.swift @@ -0,0 +1,42 @@ +import Foundation + +/// Creates HTML code elements for displaying code snippets. +/// +/// Represents a fragment of computer code, typically displayed in a monospace font. +/// Code elements are useful for inline code references, syntax highlighting, and technical documentation. +public final class Code: Element { + /// Creates a new HTML code element. + /// + /// - Parameters: + /// - id: Unique identifier for the HTML element, useful for JavaScript interaction and styling. + /// - classes: An array of CSS classnames for styling the code element. + /// - 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 code element. + /// - content: Closure providing code content. + /// + /// ## Example + /// ```swift + /// Code { + /// "let x = 42" + /// } + /// ``` + 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 + ) + } +} diff --git a/Sources/WebUI/Elements/Text/Heading/Heading.swift b/Sources/WebUI/Elements/Text/Heading/Heading.swift new file mode 100644 index 00000000..cb97b4aa --- /dev/null +++ b/Sources/WebUI/Elements/Text/Heading/Heading.swift @@ -0,0 +1,45 @@ +import Foundation + +/// Creates HTML heading elements from `

        ` to `

        `. +/// +/// Represents section headings of different levels, with `

        ` being the highest (most important) +/// and `

        ` the lowest. Headings provide document structure and are essential for accessibility, +/// SEO, and reader comprehension. +public final class Heading: Element { + /// Creates a new HTML heading element. + /// + /// - Parameters: + /// - level: Heading level (.largeTitle, .title, .headline, .subheadline, .body, or .footnote). + /// - id: Unique identifier for the HTML element, useful for JavaScript interaction and styling. + /// - classes: An array of CSS classnames for styling the heading. + /// - 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 heading. + /// - content: Closure providing heading content. + /// + /// ## Example + /// ```swift + /// Heading(.title) { + /// "Section Title" + /// } + /// ``` + 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 + ) + } +} diff --git a/Sources/WebUI/Elements/Text/Heading/HeadingLevel.swift b/Sources/WebUI/Elements/Text/Heading/HeadingLevel.swift new file mode 100644 index 00000000..dea9d150 --- /dev/null +++ b/Sources/WebUI/Elements/Text/Heading/HeadingLevel.swift @@ -0,0 +1,27 @@ +/// Defines semantic levels for HTML heading tags from h1 to h6. +/// +/// HTML headings provide document structure and establish content hierarchy. +/// Each level has a specific semantic meaning, with h1 being the most important +/// and h6 the least. Proper use of heading levels improves accessibility, +/// SEO, and overall document organization. +/// +/// ## Example +/// ```swift +/// Heading(.largeTitle) { +/// "Main Page Title" +/// } +/// ``` +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" +} diff --git a/Sources/WebUI/Styles/Color/BackgroundStyleOperation.swift b/Sources/WebUI/Styles/Color/BackgroundStyleOperation.swift new file mode 100644 index 00000000..d6717822 --- /dev/null +++ b/Sources/WebUI/Styles/Color/BackgroundStyleOperation.swift @@ -0,0 +1,107 @@ +import Foundation + +/// Style operation for background styling +/// +/// Provides a unified implementation for background styling that can be used across +/// Element methods and the Declarative DSL functions. +public struct BackgroundStyleOperation: StyleOperation, @unchecked Sendable { + /// Parameters for background styling + public struct Parameters { + /// The background color + public let color: Color + + /// Creates parameters for background styling + /// + /// - Parameters: + /// - color: The background color + public init(color: Color) { + self.color = color + } + + /// Creates parameters from a StyleParameters container + /// + /// - Parameter params: The style parameters container + /// - Returns: BackgroundStyleOperation.Parameters + public static func from(_ params: StyleParameters) -> Parameters { + Parameters( + color: params.get("color")! + ) + } + } + + /// Applies the background style and returns the appropriate CSS classes + /// + /// - Parameter params: The parameters for background styling + /// - Returns: An array of CSS class names to be applied to elements + public func applyClasses(params: Parameters) -> [String] { + ["bg-\(params.color.rawValue)"] + } + + /// Shared instance for use across the framework + public static let shared = BackgroundStyleOperation() + + /// Private initializer to enforce singleton usage + private init() {} +} + +// Extension for Element to provide background styling +extension Element { + /// Applies background color to the element. + /// + /// Adds a background color class based on the provided color and optional modifiers. + /// This method applies Tailwind CSS background color classes to the element. + /// + /// - Parameters: + /// - color: Sets the background color from the color palette or a custom value. + /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. + /// - Returns: A new element with updated background color classes. + /// + /// ## Example + /// ```swift + /// // Simple background color + /// Button() { "Submit" } + /// .background(color: .green(._500)) + /// + /// // Background color with modifiers + /// Button() { "Hover me" } + /// .background(color: .white, on: .dark) + /// .background(color: .blue(._500), on: .hover) + /// ``` + public func background( + color: Color, + on modifiers: Modifier... + ) -> Element { + let params = BackgroundStyleOperation.Parameters(color: color) + + return BackgroundStyleOperation.shared.applyToElement( + self, + params: params, + modifiers: modifiers + ) + } +} + +// Extension for ResponsiveBuilder to provide background styling +extension ResponsiveBuilder { + /// Applies background color in a responsive context. + /// + /// - Parameter color: The background color. + /// - Returns: The builder for method chaining. + @discardableResult + public func background(color: Color) -> ResponsiveBuilder { + let params = BackgroundStyleOperation.Parameters(color: color) + + return BackgroundStyleOperation.shared.applyToBuilder(self, params: params) + } +} + +// Global function for Declarative DSL +/// Applies background color styling in the responsive context. +/// +/// - Parameter color: The background color. +/// - Returns: A responsive modification for background color. +public func background(color: Color) -> ResponsiveModification { + let params = BackgroundStyleOperation.Parameters(color: color) + + return BackgroundStyleOperation.shared.asModification(params: params) +} diff --git a/Sources/WebUI/Styles/Color/Border/BorderStyleOperation.swift b/Sources/WebUI/Styles/Color/Border/BorderStyleOperation.swift new file mode 100644 index 00000000..bc2c3970 --- /dev/null +++ b/Sources/WebUI/Styles/Color/Border/BorderStyleOperation.swift @@ -0,0 +1,213 @@ +import Foundation + +/// Style operation for border styling +/// +/// Provides a unified implementation for border styling that can be used across +/// Element methods and the Declaritive DSL functions. +public struct BorderStyleOperation: StyleOperation, @unchecked Sendable { + /// Parameters for border styling + public struct Parameters { + /// The border width + public let width: Int? + + /// The edges to apply the border to + public let edges: [Edge] + + /// The border style + public let style: BorderStyle? + + /// The border color + public let color: Color? + + /// Creates parameters for border styling + /// + /// - Parameters: + /// - width: The border width + /// - edges: The edges to apply the border to + /// - style: The border style + /// - color: The border color + public init( + width: Int? = 1, + edges: [Edge] = [.all], + style: BorderStyle? = nil, + color: Color? = nil + ) { + self.width = width + self.edges = edges.isEmpty ? [.all] : edges + self.style = style + self.color = color + } + + /// Creates parameters from a StyleParameters container + /// + /// - Parameter params: The style parameters container + /// - Returns: BorderStyleOperation.Parameters + public static func from(_ params: StyleParameters) -> Parameters { + Parameters( + width: params.get("width"), + edges: params.get("edges", default: [.all]), + style: params.get("style"), + color: params.get("color") + ) + } + } + + /// Applies the border style and returns the appropriate CSS classes + /// + /// - Parameter params: The parameters for border styling + /// - Returns: An array of CSS class names to be applied to elements + public func applyClasses(params: Parameters) -> [String] { + var classes: [String] = [] + let width = params.width + let edges = params.edges + let style = params.style + let color = params.color + + for edge in edges { + if let style = style, style == .divide { + if let width = width { + let divideClass = edge == .horizontal ? "divide-x-\(width)" : "divide-y-\(width)" + classes.append(divideClass) + } + } else { + let prefix = edge == .all ? "border" : "border-\(edge.rawValue)" + if let width = width { + classes.append("\(prefix)-\(width)") + } else { + classes.append(prefix) + } + } + } + + if let style = style, style != .divide { + classes.append("border-\(style.rawValue)") + } + + if let color = color { + classes.append("border-\(color.rawValue)") + } + + return classes + } + + /// Shared instance for use across the framework + public static let shared = BorderStyleOperation() + + /// Private initializer to enforce singleton usage + private init() {} +} + +// Extension for Element to provide border styling +extension Element { + /// Applies border styling to the element with specified attributes. + /// + /// Adds borders with custom width, style, and color to specified edges of an element. + /// + /// - Parameters: + /// - width: The border width in pixels. + /// - edges: One or more edges to apply the border to. Defaults to `.all`. + /// - style: The border style (solid, dashed, etc.). + /// - color: The border color. + /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. + /// - Returns: A new element with updated border classes. + /// + /// ## Example + /// ```swift + /// Stack() + /// .border(of: 2, at: .bottom, color: .blue(._500)) + /// .border(of: 1, at: .horizontal, color: .gray(._200), on: .hover) + /// ``` + public func border( + of width: Int? = nil, + at edges: Edge..., + style: BorderStyle? = nil, + color: Color? = nil, + on modifiers: Modifier... + ) -> Element { + let params = BorderStyleOperation.Parameters( + width: width, + edges: edges, + style: style, + color: color + ) + + return BorderStyleOperation.shared.applyToElement( + self, + params: params, + modifiers: modifiers + ) + } +} + +// Extension for ResponsiveBuilder to provide border styling +extension ResponsiveBuilder { + /// Applies border styling in a responsive context. + /// + /// - Parameters: + /// - width: The border width in pixels. + /// - edges: One or more edges to apply the border to. Defaults to `.all`. + /// - style: The border style (solid, dashed, etc.). + /// - color: The border color. + /// - Returns: The builder for method chaining. + @discardableResult + public func border( + of width: Int? = 1, + at edges: Edge..., + style: BorderStyle? = nil, + color: Color? = nil + ) -> ResponsiveBuilder { + let params = BorderStyleOperation.Parameters( + width: width, + edges: edges, + style: style, + color: color + ) + + return BorderStyleOperation.shared.applyToBuilder(self, params: params) + } + + /// Helper method to apply just a border style. + /// + /// - Parameter style: The border style to apply. + /// - Returns: The builder for method chaining. + @discardableResult + public func border(style: BorderStyle) -> ResponsiveBuilder { + let params = BorderStyleOperation.Parameters(style: style) + return BorderStyleOperation.shared.applyToBuilder(self, params: params) + } + + /// Helper method to apply just a border color. + /// + /// - Parameter color: The border color to apply. + /// - Returns: The builder for method chaining. + @discardableResult + public func border(color: Color) -> ResponsiveBuilder { + let params = BorderStyleOperation.Parameters(color: color) + return BorderStyleOperation.shared.applyToBuilder(self, params: params) + } +} + +// Global function for Declaritive DSL +/// Applies border styling in the responsive context. +/// +/// - Parameters: +/// - width: The border width. +/// - edges: The edges to apply the border to. +/// - style: The border style. +/// - color: The border color. +/// - Returns: A responsive modification for borders. +public func border( + of width: Int? = 1, + at edges: Edge..., + style: BorderStyle? = nil, + color: Color? = nil +) -> ResponsiveModification { + let params = BorderStyleOperation.Parameters( + width: width, + edges: edges, + style: style, + color: color + ) + + return BorderStyleOperation.shared.asModification(params: params) +} diff --git a/Sources/WebUI/Styles/Color/Border/BorderTypes.swift b/Sources/WebUI/Styles/Color/Border/BorderTypes.swift new file mode 100644 index 00000000..f6dbc478 --- /dev/null +++ b/Sources/WebUI/Styles/Color/Border/BorderTypes.swift @@ -0,0 +1,85 @@ +/// Defines sides for applying border radius. +/// +/// Represents individual corners or groups of corners for styling border radius. +/// +/// ## Example +/// ```swift +/// Button() { "Sign Up" } +/// .rounded(.lg, .top) +/// ``` +public enum RadiusSide: String { + /// Applies radius to all corners + case all = "" + /// Applies radius to the top side (top-left and top-right) + case top = "t" + /// Applies radius to the right side (top-right and bottom-right) + case right = "r" + /// Applies radius to the bottom side (bottom-left and bottom-right) + case bottom = "b" + /// Applies radius to the left side (top-left and bottom-left) + case left = "l" + /// Applies radius to the top-left corner + case topLeft = "tl" + /// Applies radius to the top-right corner + case topRight = "tr" + /// Applies radius to the bottom-left corner + case bottomLeft = "bl" + /// Applies radius to the bottom-right corner + case bottomRight = "br" +} + +/// Specifies sizes for border radius. +/// +/// Defines a range of radius values from none to full circular. +/// +/// ## Example +/// ```swift +/// Stack(classes: ["card"]) +/// .rounded(.xl) +/// ``` +public enum RadiusSize: String { + /// No border radius (0) + case none = "none" + /// Extra small radius (0.125rem) + case xs = "xs" + /// Small radius (0.25rem) + case sm = "sm" + /// Medium radius (0.375rem) + case md = "md" + /// Large radius (0.5rem) + case lg = "lg" + /// Extra large radius (0.75rem) + case xl = "xl" + /// 2x large radius (1rem) + case xl2 = "2xl" + /// 3x large radius (1.5rem) + case xl3 = "3xl" + /// Full radius (9999px, circular) + case full = "full" +} + +/// Defines styles for borders and outlines. +/// +/// Provides options for solid, dashed, and other border appearances. +/// +/// ## Example +/// ```swift +/// Stack() +/// .border(width: 1, style: .dashed, color: .gray(._300)) +/// ``` +public enum BorderStyle: String { + /// Solid line border + case solid = "solid" + /// Dashed line border + case dashed = "dashed" + /// Dotted line border + case dotted = "dotted" + /// Double line border + case double = "double" + /// Hidden border (none) + case hidden = "hidden" + /// No border (none) + case none = "none" + /// Divider style for child elements + case divide = "divide" +} diff --git a/Sources/WebUI/Styles/Color/RingStyleOperation.swift b/Sources/WebUI/Styles/Color/RingStyleOperation.swift new file mode 100644 index 00000000..7fe91b7b --- /dev/null +++ b/Sources/WebUI/Styles/Color/RingStyleOperation.swift @@ -0,0 +1,160 @@ +// +// RingStyleOperation.swift +// web-ui +// +// Created by Mac Long on 2025.05.22. +// + +import Foundation + +/// Style operation for ring styling +/// +/// Provides a unified implementation for ring styling that can be used across +/// Element methods and the Declaritive DSL functions. +public struct RingStyleOperation: StyleOperation, @unchecked Sendable { + /// Parameters for ring styling + public struct Parameters { + /// The ring width + public let size: Int? + + /// The ring color + public let color: Color? + + /// Creates parameters for ring styling + /// + /// - Parameters: + /// - size: the width of the ring + /// - color: The ring color + public init( + size: Int? = 1, + color: Color? = nil + ) { + self.size = size + self.color = color + } + + /// Creates parameters from a StyleParameters container + /// + /// - Parameter params: The style parameters container + /// - Returns: RingStyleOperation.Parameters + public static func from(_ params: StyleParameters) -> Parameters { + Parameters( + size: params.get("size"), + color: params.get("color") + ) + } + } + + /// Applies the ring style and returns the appropriate CSS classes + /// + /// - Parameter params: The parameters for ring styling + /// - Returns: An array of CSS class names to be applied to elements + public func applyClasses(params: Parameters) -> [String] { + var classes: [String] = [] + let size = params.size ?? 1 + let color = params.color + + classes.append("ring-\(size)") + + if let color = color { + classes.append("ring-\(color.rawValue)") + } + + return classes + } + + /// Shared instance for use across the framework + public static let shared = RingStyleOperation() + + /// Private initializer to enforce singleton usage + private init() {} +} + +// Extension for Element to provide ring styling +extension Element { + /// Applies ring styling to the element with specified attributes. + /// + /// Adds rings with custom width, style, and color to specified edges of an element. + /// + /// - Parameters: + /// - width: The ring width in pixels. + /// - edges: One or more edges to apply the ring to. Defaults to `.all`. + /// - style: The ring style (solid, dashed, etc.). + /// - color: The ring color. + /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. + /// - Returns: A new element with updated ring classes. + /// + /// ## Example + /// ```swift + /// Stack() + /// .ring(of: 2, at: .bottom, color: .blue(._500)) + /// .ring(of: 1, at: .horizontal, color: .gray(._200), on: .hover) + /// ``` + public func ring( + size: Int = 1, + color: Color? = nil, + on modifiers: Modifier... + ) -> Element { + let params = RingStyleOperation.Parameters( + size: size, + color: color + ) + + return RingStyleOperation.shared.applyToElement( + self, + params: params, + modifiers: modifiers + ) + } +} + +// Extension for ResponsiveBuilder to provide ring styling +extension ResponsiveBuilder { + /// Applies ring styling in a responsive context. + /// + /// - Parameters: + /// - size: The width of the ring. + /// - color: The ring color. + /// - Returns: The builder for method chaining. + @discardableResult + public func ring( + size: Int = 1, + color: Color? = nil + ) -> ResponsiveBuilder { + let params = RingStyleOperation.Parameters( + size: size, + color: color + ) + + return RingStyleOperation.shared.applyToBuilder(self, params: params) + } + + /// Helper method to apply just a ring color. + /// + /// - Parameter color: The ring color to apply. + /// - Returns: The builder for method chaining. + @discardableResult + public func ring(color: Color) -> ResponsiveBuilder { + let params = RingStyleOperation.Parameters(color: color) + return RingStyleOperation.shared.applyToBuilder(self, params: params) + } +} + +// Global function for Declaritive DSL +/// Applies ring styling in the responsive context. +/// +/// - Parameters: +/// - size: The width of the ring. +/// - color: The ring color. +/// - Returns: A responsive modification for rings. +public func ring( + of size: Int = 1, + color: Color? = nil +) -> ResponsiveModification { + let params = RingStyleOperation.Parameters( + size: size, + color: color + ) + + return RingStyleOperation.shared.asModification(params: params) +} diff --git a/Sources/WebUI/Styles/Base/Utilities.swift b/Sources/WebUI/Styles/Core/Utilities.swift similarity index 98% rename from Sources/WebUI/Styles/Base/Utilities.swift rename to Sources/WebUI/Styles/Core/Utilities.swift index a4068316..93b6561a 100644 --- a/Sources/WebUI/Styles/Base/Utilities.swift +++ b/Sources/WebUI/Styles/Core/Utilities.swift @@ -292,6 +292,16 @@ public func combineClasses(_ baseClasses: [String], withModifiers modifiers: [Mo /// .font(color: .white) /// ``` public enum Color { + /// Pure white #ffffff color with optional opacity. + /// + /// A bright white color with no hue or saturatio + case white(opacity: Double? = nil) + + /// Pure black #000000 color with optional opacity. + /// + /// A deep black color with no hue or saturation. + case black(opacity: Double? = nil) + /// A slate gray color with varying intensity shades and optional opacity. /// /// A cool gray with subtle blue undertones. diff --git a/Sources/WebUI/Styles/Effects/BorderRadiusStyleOperation.swift b/Sources/WebUI/Styles/Effects/BorderRadiusStyleOperation.swift new file mode 100644 index 00000000..86ec1c58 --- /dev/null +++ b/Sources/WebUI/Styles/Effects/BorderRadiusStyleOperation.swift @@ -0,0 +1,143 @@ +import Foundation + +/// Style operation for border radius styling +/// +/// Provides a unified implementation for border radius styling that can be used across +/// Element methods and the Declarative DSL functions. +public struct BorderRadiusStyleOperation: StyleOperation, @unchecked Sendable { + /// Parameters for border radius styling + public struct Parameters { + /// The border radius size + public let size: RadiusSize? + + /// The sides to apply the radius to + public let sides: [RadiusSide] + + /// Creates parameters for border radius styling + /// + /// - Parameters: + /// - size: The border radius size + /// - sides: The sides to apply the radius to + public init( + size: RadiusSize? = .md, + sides: [RadiusSide] = [.all] + ) { + self.size = size + self.sides = sides.isEmpty ? [.all] : sides + } + + /// Creates parameters from a StyleParameters container + /// + /// - Parameter params: The style parameters container + /// - Returns: BorderRadiusStyleOperation.Parameters + public static func from(_ params: StyleParameters) -> Parameters { + Parameters( + size: params.get("size"), + sides: params.get("sides", default: [.all]) + ) + } + } + + /// Applies the border radius style and returns the appropriate CSS classes + /// + /// - Parameter params: The parameters for border radius styling + /// - Returns: An array of CSS class names to be applied to elements + public func applyClasses(params: Parameters) -> [String] { + var classes: [String] = [] + let size = params.size + let sides = params.sides + + for side in sides { + let sidePrefix = side == .all ? "" : "-\(side.rawValue)" + let sizeValue = size != nil ? "-\(size!.rawValue)" : "" + + classes.append("rounded\(sidePrefix)\(sizeValue)") + } + + return classes + } + + /// Shared instance for use across the framework + public static let shared = BorderRadiusStyleOperation() + + /// Private initializer to enforce singleton usage + private init() {} +} + +// Extension for Element to provide border radius styling +extension Element { + /// Applies border radius styling to the element. + /// + /// - Parameters: + /// - size: The radius size from none to full. + /// - sides: Zero or more sides to apply the radius to. Defaults to all sides. + /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. + /// - Returns: A new element with updated border radius classes. + /// + /// ## Example + /// ```swift + /// Button() { "Sign Up" } + /// .rounded(.lg) + /// + /// Stack(classes: ["card"]) + /// .rounded(.xl, .top) + /// .rounded(.lg, .bottom, on: .hover) + /// ``` + public func rounded( + _ size: RadiusSize? = .md, + _ sides: RadiusSide..., + on modifiers: Modifier... + ) -> Element { + let params = BorderRadiusStyleOperation.Parameters( + size: size, + sides: sides + ) + + return BorderRadiusStyleOperation.shared.applyToElement( + self, + params: params, + modifiers: modifiers + ) + } +} + +// Extension for ResponsiveBuilder to provide border radius styling +extension ResponsiveBuilder { + /// Applies border radius styling in a responsive context. + /// + /// - Parameters: + /// - size: The radius size from none to full. + /// - sides: Zero or more sides to apply the radius to. + /// - Returns: The builder for method chaining. + @discardableResult + public func rounded( + _ size: RadiusSize? = .md, + _ sides: RadiusSide... + ) -> ResponsiveBuilder { + let params = BorderRadiusStyleOperation.Parameters( + size: size, + sides: sides + ) + + return BorderRadiusStyleOperation.shared.applyToBuilder(self, params: params) + } +} + +// Global function for Declarative DSL +/// Applies border radius styling in the responsive context. +/// +/// - Parameters: +/// - size: The radius size from none to full. +/// - sides: The sides to apply the radius to. +/// - Returns: A responsive modification for border radius. +public func rounded( + _ size: RadiusSize? = .md, + _ sides: RadiusSide... +) -> ResponsiveModification { + let params = BorderRadiusStyleOperation.Parameters( + size: size, + sides: sides + ) + + return BorderRadiusStyleOperation.shared.asModification(params: params) +} diff --git a/Sources/WebUI/Styles/Effects/OpacityStyleOperation.swift b/Sources/WebUI/Styles/Effects/OpacityStyleOperation.swift new file mode 100644 index 00000000..783325f4 --- /dev/null +++ b/Sources/WebUI/Styles/Effects/OpacityStyleOperation.swift @@ -0,0 +1,98 @@ +import Foundation + +/// Style operation for opacity styling +/// +/// Provides a unified implementation for opacity styling that can be used across +/// Element methods and the Declarative DSL functions. +public struct OpacityStyleOperation: StyleOperation, @unchecked Sendable { + /// Parameters for opacity styling + public struct Parameters { + /// The opacity value (0-100) + public let value: Int + + /// Creates parameters for opacity styling + /// + /// - Parameter value: The opacity value (0-100) + public init(value: Int) { + self.value = value + } + + /// Creates parameters from a StyleParameters container + /// + /// - Parameter params: The style parameters container + /// - Returns: OpacityStyleOperation.Parameters + public static func from(_ params: StyleParameters) -> Parameters { + Parameters( + value: params.get("value")! + ) + } + } + + /// Applies the opacity style and returns the appropriate CSS classes + /// + /// - Parameter params: The parameters for opacity styling + /// - Returns: An array of CSS class names to be applied to elements + public func applyClasses(params: Parameters) -> [String] { + ["opacity-\(params.value)"] + } + + /// Shared instance for use across the framework + public static let shared = OpacityStyleOperation() + + /// Private initializer to enforce singleton usage + private init() {} +} + +// Extension for Element to provide opacity styling +extension Element { + /// Sets the opacity of the element with optional modifiers. + /// + /// - Parameters: + /// - value: The opacity value, typically between 0 and 100. + /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. + /// - Returns: A new element with updated opacity classes including applied modifiers. + /// + /// ## Example + /// ```swift + /// Image(source: "/images/profile.jpg", description: "Profile Photo") + /// .opacity(50) + /// .opacity(100, on: .hover) + /// ``` + public func opacity( + _ value: Int, + on modifiers: Modifier... + ) -> Element { + let params = OpacityStyleOperation.Parameters(value: value) + + return OpacityStyleOperation.shared.applyToElement( + self, + params: params, + modifiers: modifiers + ) + } +} + +// Extension for ResponsiveBuilder to provide opacity styling +extension ResponsiveBuilder { + /// Applies opacity styling in a responsive context. + /// + /// - Parameter value: The opacity value (0-100). + /// - Returns: The builder for method chaining. + @discardableResult + public func opacity(_ value: Int) -> ResponsiveBuilder { + let params = OpacityStyleOperation.Parameters(value: value) + + return OpacityStyleOperation.shared.applyToBuilder(self, params: params) + } +} + +// Global function for Declarative DSL +/// Applies opacity styling in the responsive context. +/// +/// - Parameter value: The opacity value (0-100). +/// - Returns: A responsive modification for opacity. +public func opacity(_ value: Int) -> ResponsiveModification { + let params = OpacityStyleOperation.Parameters(value: value) + + return OpacityStyleOperation.shared.asModification(params: params) +} diff --git a/Sources/WebUI/Styles/Effects/OutlineStyleOperation.swift b/Sources/WebUI/Styles/Effects/OutlineStyleOperation.swift new file mode 100644 index 00000000..5d279fc7 --- /dev/null +++ b/Sources/WebUI/Styles/Effects/OutlineStyleOperation.swift @@ -0,0 +1,188 @@ +import Foundation + +/// Style operation for outline styling +/// +/// Provides a unified implementation for outline styling that can be used across +/// Element methods and the Declarative DSL functions. +public struct OutlineStyleOperation: StyleOperation, @unchecked Sendable { + /// Parameters for outline styling + public struct Parameters { + /// The outline width + public let width: Int? + + /// The outline style (solid, dashed, etc.) + public let style: BorderStyle? + + /// The outline color + public let color: Color? + + /// The outline offset + public let offset: Int? + + /// Creates parameters for outline styling + /// + /// - Parameters: + /// - width: The outline width + /// - style: The outline style + /// - color: The outline color + /// - offset: The outline offset + public init( + width: Int? = nil, + style: BorderStyle? = nil, + color: Color? = nil, + offset: Int? = nil + ) { + self.width = width + self.style = style + self.color = color + self.offset = offset + } + + /// Creates parameters from a StyleParameters container + /// + /// - Parameter params: The style parameters container + /// - Returns: OutlineStyleOperation.Parameters + public static func from(_ params: StyleParameters) -> Parameters { + Parameters( + width: params.get("width"), + style: params.get("style"), + color: params.get("color"), + offset: params.get("offset") + ) + } + } + + /// Applies the outline style and returns the appropriate CSS classes + /// + /// - Parameter params: The parameters for outline styling + /// - Returns: An array of CSS class names to be applied to elements + public func applyClasses(params: Parameters) -> [String] { + var classes = [String]() + + if let width = params.width { + classes.append("outline-\(width)") + } + + if let style = params.style { + classes.append("outline-\(style.rawValue)") + } + + if let color = params.color { + classes.append("outline-\(color.rawValue)") + } + + if let offset = params.offset { + classes.append("outline-offset-\(offset)") + } + + if classes.isEmpty { + classes.append("outline") + } + + return classes + } + + /// Shared instance for use across the framework + public static let shared = OutlineStyleOperation() + + /// Private initializer to enforce singleton usage + private init() {} +} + +// Extension for Element to provide outline styling +extension Element { + /// Sets outline properties with optional modifiers. + /// + /// - Parameters: + /// - width: The outline width. + /// - style: The outline style (solid, dashed, etc.). + /// - color: The outline color. + /// - offset: The outline offset in pixels. + /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. + /// - Returns: A new element with updated outline classes. + /// + /// ## Example + /// ```swift + /// // Add a basic outline + /// Element(tag: "div").outline() + /// + /// // Add a 2px outline with color + /// Element(tag: "div").outline(of: 2, color: .blue(._500)) + /// + /// // Add a dashed outline on focus + /// Element(tag: "div").outline(style: .dashed, on: .focus) + /// ``` + public func outline( + of width: Int? = nil, + style: BorderStyle? = nil, + color: Color? = nil, + offset: Int? = nil, + on modifiers: Modifier... + ) -> Element { + let params = OutlineStyleOperation.Parameters( + width: width, + style: style, + color: color, + offset: offset + ) + + return OutlineStyleOperation.shared.applyToElement( + self, + params: params, + modifiers: modifiers + ) + } +} + +// Extension for ResponsiveBuilder to provide outline styling +extension ResponsiveBuilder { + /// Sets outline properties in a responsive context. + /// + /// - Parameters: + /// - width: The outline width. + /// - style: The outline style (solid, dashed, etc.). + /// - color: The outline color. + /// - offset: The outline offset in pixels. + /// - Returns: The builder for method chaining. + @discardableResult + public func outline( + of width: Int? = nil, + style: BorderStyle? = nil, + color: Color? = nil, + offset: Int? = nil + ) -> ResponsiveBuilder { + let params = OutlineStyleOperation.Parameters( + width: width, + style: style, + color: color, + offset: offset + ) + + return OutlineStyleOperation.shared.applyToBuilder(self, params: params) + } +} + +// Global function for Declarative DSL +/// Sets outline properties in the responsive context. +/// +/// - Parameters: +/// - width: The outline width. +/// - style: The outline style (solid, dashed, etc.). +/// - color: The outline color. +/// - offset: The outline offset in pixels. +/// - Returns: A responsive modification for outline. +public func outline( + of width: Int? = nil, + style: BorderStyle? = nil, + color: Color? = nil, + offset: Int? = nil +) -> ResponsiveModification { + let params = OutlineStyleOperation.Parameters( + width: width, + style: style, + color: color, + offset: offset + ) + + return OutlineStyleOperation.shared.asModification(params: params) +} \ No newline at end of file diff --git a/Sources/WebUI/Styles/Effects/Shadow/ShadowStyleOperation.swift b/Sources/WebUI/Styles/Effects/Shadow/ShadowStyleOperation.swift new file mode 100644 index 00000000..8f69cb35 --- /dev/null +++ b/Sources/WebUI/Styles/Effects/Shadow/ShadowStyleOperation.swift @@ -0,0 +1,155 @@ +import Foundation + +/// Style operation for box shadow styling +/// +/// Provides a unified implementation for box shadow styling that can be used across +/// Element methods and the Declaritive DSL functions. +public struct ShadowStyleOperation: StyleOperation, @unchecked Sendable { + /// Parameters for shadow styling + public struct Parameters { + /// The shadow size + public let size: ShadowSize? + + /// The shadow color + public let color: Color? + + /// Creates parameters for shadow styling + /// + /// - Parameters: + /// - size: The shadow size + /// - color: The shadow color + public init( + size: ShadowSize? = nil, + color: Color? = nil + ) { + self.size = size + self.color = color + } + + /// Creates parameters from a StyleParameters container + /// + /// - Parameter params: The style parameters container + /// - Returns: ShadowStyleOperation.Parameters + public static func from(_ params: StyleParameters) -> Parameters { + Parameters( + size: params.get("size"), + color: params.get("color") + ) + } + } + + /// Applies the shadow style and returns the appropriate CSS classes + /// + /// - Parameter params: The parameters for shadow styling + /// - Returns: An array of CSS class names to be applied to elements + public func applyClasses(params: Parameters) -> [String] { + var classes: [String] = [] + let size = params.size ?? ShadowSize.md + let color = params.color + + classes.append("shadow-\(size.rawValue)") + + if let color = color { + classes.append("shadow-\(color.rawValue)") + } + + return classes + } + + /// Shared instance for use across the framework + public static let shared = ShadowStyleOperation() + + /// Private initializer to enforce singleton usage + private init() {} +} + +// Extension for Element to provide shadow styling +extension Element { + /// Applies shadow styling to the element with specified attributes. + /// + /// Adds shadows with custom size and color to an element. + /// + /// - Parameters: + /// - size: The shadow size (sm, md, lg). + /// - color: The shadow color. + /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. + /// - Returns: A new element with updated shadow classes. + /// + /// ## Example + /// ```swift + /// Stack() + /// .shadow(size: .lg, color: .blue(._500)) + /// .shadow(size: .sm, color: .gray(._200), on: .hover) + /// ``` + public func shadow( + size: ShadowSize, + color: Color? = nil, + on modifiers: Modifier... + ) -> Element { + let params = ShadowStyleOperation.Parameters( + size: size, + color: color + ) + + return ShadowStyleOperation.shared.applyToElement( + self, + params: params, + modifiers: modifiers + ) + } +} + +// Extension for ResponsiveBuilder to provide shadow styling +extension ResponsiveBuilder { + /// Applies shadow styling in a responsive context. + /// + /// - Parameters: + /// - size: The shadow size. + /// - color: The shadow color. + /// - Returns: The builder for method chaining. + @discardableResult + public func shadow( + size: ShadowSize? = .md, + color: Color? = nil, + ) -> ResponsiveBuilder { + let params = ShadowStyleOperation.Parameters( + size: size, + color: color + ) + + return ShadowStyleOperation.shared.applyToBuilder(self, params: params) + } + + /// Helper method to apply just a shadow color. + /// + /// - Parameter color: The shadow color to apply. + /// - Returns: The builder for method chaining. + @discardableResult + public func shadow(color: Color) -> ResponsiveBuilder { + let params = ShadowStyleOperation.Parameters( + size: .md, + color: color + ) + + return ShadowStyleOperation.shared.applyToBuilder(self, params: params) + } +} + +// Global function for Declaritive DSL +/// Applies shadow styling in the responsive context. +/// +/// - Parameters: +/// - size: The shadow size. +/// - color: The shadow color. +/// - Returns: A responsive modification for shadows. +public func shadow( + of size: ShadowSize? = .md, + color: Color? = nil +) -> ResponsiveModification { + let params = ShadowStyleOperation.Parameters( + size: size, + color: color + ) + + return ShadowStyleOperation.shared.asModification(params: params) +} diff --git a/Sources/WebUI/Styles/Effects/Shadow/ShadowTypes.swift b/Sources/WebUI/Styles/Effects/Shadow/ShadowTypes.swift new file mode 100644 index 00000000..5f97b5b0 --- /dev/null +++ b/Sources/WebUI/Styles/Effects/Shadow/ShadowTypes.swift @@ -0,0 +1,27 @@ +/// Specifies sizes for box shadows. +/// +/// Defines shadow sizes from none to extra-large. +/// +/// ## Example +/// ```swift +/// Stack(classes: ["card"]) +/// .shadow(size: .lg, color: .gray(._300, opacity: 0.5)) +/// ``` +public enum ShadowSize: String { + /// No shadow + case none = "none" + /// Extra small shadow (2xs) + case xs2 = "2xs" + /// Extra small shadow (xs) + case xs = "xs" + /// Small shadow (sm) + case sm = "sm" + /// Medium shadow (default) + case md = "md" + /// Large shadow (lg) + case lg = "lg" + /// Extra large shadow (xl) + case xl = "xl" + /// 2x large shadow (2xl) + case xl2 = "2xl" +} diff --git a/Sources/WebUI/Styles/Positioning/TransformStyleOperation.swift b/Sources/WebUI/Styles/Effects/TransformStyleOperation.swift similarity index 100% rename from Sources/WebUI/Styles/Positioning/TransformStyleOperation.swift rename to Sources/WebUI/Styles/Effects/TransformStyleOperation.swift diff --git a/Sources/WebUI/Styles/Positioning/Transition/TransitionStyleOperation.swift b/Sources/WebUI/Styles/Effects/Transition/TransitionStyleOperation.swift similarity index 100% rename from Sources/WebUI/Styles/Positioning/Transition/TransitionStyleOperation.swift rename to Sources/WebUI/Styles/Effects/Transition/TransitionStyleOperation.swift diff --git a/Sources/WebUI/Styles/Positioning/Transition/TransitionTypes.swift b/Sources/WebUI/Styles/Effects/Transition/TransitionTypes.swift similarity index 100% rename from Sources/WebUI/Styles/Positioning/Transition/TransitionTypes.swift rename to Sources/WebUI/Styles/Effects/Transition/TransitionTypes.swift diff --git a/Sources/WebUI/Styles/Base/CursorStyleOperation.swift b/Sources/WebUI/Styles/Interactivity/CursorStyleOperation.swift similarity index 100% rename from Sources/WebUI/Styles/Base/CursorStyleOperation.swift rename to Sources/WebUI/Styles/Interactivity/CursorStyleOperation.swift diff --git a/Sources/WebUI/Styles/Interactivity/InteractionModifiers.swift b/Sources/WebUI/Styles/Interactivity/InteractionModifiers.swift new file mode 100644 index 00000000..ed1f2f1e --- /dev/null +++ b/Sources/WebUI/Styles/Interactivity/InteractionModifiers.swift @@ -0,0 +1,370 @@ +import Foundation + +/// Provides support for interactive states and states modifiers in the WebUI framework. +/// +/// This extension adds support for additional modifiers like hover, focus, and other +/// interactive states to the ResponsiveBuilder to allow styling based on element state. + +extension ResponsiveBuilder { + /// Applies styles when the element is hovered. + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func hover(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .hover + modifications(self) + applyBreakpoint() + return self + } + + /// Applies styles when the element has keyboard focus. + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func focus(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .focus + modifications(self) + applyBreakpoint() + return self + } + + /// Applies styles when the element is being actively pressed or clicked. + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func active(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .active + modifications(self) + applyBreakpoint() + return self + } + + /// Applies styles to input placeholders within the element. + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func placeholder(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .placeholder + modifications(self) + applyBreakpoint() + return self + } + + /// Applies styles when dark mode is active. + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func dark(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .dark + modifications(self) + applyBreakpoint() + return self + } + + /// Applies styles to the first child element. + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func first(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .first + modifications(self) + applyBreakpoint() + return self + } + + /// Applies styles to the last child element. + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func last(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .last + modifications(self) + applyBreakpoint() + return self + } + + /// Applies styles when the element is disabled. + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func disabled(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .disabled + modifications(self) + applyBreakpoint() + return self + } + + /// Applies styles when the user prefers reduced motion. + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func motionReduce(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .motionReduce + modifications(self) + applyBreakpoint() + return self + } + + /// Applies styles when the element has aria-busy="true". + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func ariaBusy(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .ariaBusy + modifications(self) + applyBreakpoint() + return self + } + + /// Applies styles when the element has aria-checked="true". + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func ariaChecked(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .ariaChecked + modifications(self) + applyBreakpoint() + return self + } + + /// Applies styles when the element has aria-disabled="true". + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func ariaDisabled(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .ariaDisabled + modifications(self) + applyBreakpoint() + return self + } + + /// Applies styles when the element has aria-expanded="true". + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func ariaExpanded(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .ariaExpanded + modifications(self) + applyBreakpoint() + return self + } + + /// Applies styles when the element has aria-hidden="true". + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func ariaHidden(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .ariaHidden + modifications(self) + applyBreakpoint() + return self + } + + /// Applies styles when the element has aria-pressed="true". + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func ariaPressed(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .ariaPressed + modifications(self) + applyBreakpoint() + return self + } + + /// Applies styles when the element has aria-readonly="true". + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func ariaReadonly(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .ariaReadonly + modifications(self) + applyBreakpoint() + return self + } + + /// Applies styles when the element has aria-required="true". + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func ariaRequired(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .ariaRequired + modifications(self) + applyBreakpoint() + return self + } + + /// Applies styles when the element has aria-selected="true". + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func ariaSelected(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .ariaSelected + modifications(self) + applyBreakpoint() + return self + } +} + +// MARK: - Responsive DSL Functions + +/// Creates a hover state responsive modification. +/// +/// - Parameter content: A closure containing style modifications for hover state. +/// - Returns: A responsive modification for the hover state. +public func hover(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .hover, styleModification: content()) +} + +/// Creates a focus state responsive modification. +/// +/// - Parameter content: A closure containing style modifications for focus state. +/// - Returns: A responsive modification for the focus state. +public func focus(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .focus, styleModification: content()) +} + +/// Creates an active state responsive modification. +/// +/// - Parameter content: A closure containing style modifications for active state. +/// - Returns: A responsive modification for the active state. +public func active(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .active, styleModification: content()) +} + +/// Creates a placeholder responsive modification. +/// +/// - Parameter content: A closure containing style modifications for placeholder text. +/// - Returns: A responsive modification for placeholder text. +public func placeholder(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .placeholder, styleModification: content()) +} + +/// Creates a dark mode responsive modification. +/// +/// - Parameter content: A closure containing style modifications for dark mode. +/// - Returns: A responsive modification for dark mode. +public func dark(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .dark, styleModification: content()) +} + +/// Creates a first-child responsive modification. +/// +/// - Parameter content: A closure containing style modifications for first child elements. +/// - Returns: A responsive modification for first child elements. +public func first(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .first, styleModification: content()) +} + +/// Creates a last-child responsive modification. +/// +/// - Parameter content: A closure containing style modifications for last child elements. +/// - Returns: A responsive modification for last child elements. +public func last(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .last, styleModification: content()) +} + +/// Creates a disabled state responsive modification. +/// +/// - Parameter content: A closure containing style modifications for disabled state. +/// - Returns: A responsive modification for the disabled state. +public func disabled(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .disabled, styleModification: content()) +} + +/// Creates a motion-reduce responsive modification. +/// +/// - Parameter content: A closure containing style modifications for when users prefer reduced motion. +/// - Returns: A responsive modification for reduced motion preferences. +public func motionReduce(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .motionReduce, styleModification: content()) +} + +/// Creates an aria-busy responsive modification. +/// +/// - Parameter content: A closure containing style modifications for aria-busy="true". +/// - Returns: A responsive modification for the aria-busy state. +public func ariaBusy(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .ariaBusy, styleModification: content()) +} + +/// Creates an aria-checked responsive modification. +/// +/// - Parameter content: A closure containing style modifications for aria-checked="true". +/// - Returns: A responsive modification for the aria-checked state. +public func ariaChecked(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .ariaChecked, styleModification: content()) +} + +/// Creates an aria-disabled responsive modification. +/// +/// - Parameter content: A closure containing style modifications for aria-disabled="true". +/// - Returns: A responsive modification for the aria-disabled state. +public func ariaDisabled(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .ariaDisabled, styleModification: content()) +} + +/// Creates an aria-expanded responsive modification. +/// +/// - Parameter content: A closure containing style modifications for aria-expanded="true". +/// - Returns: A responsive modification for the aria-expanded state. +public func ariaExpanded(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .ariaExpanded, styleModification: content()) +} + +/// Creates an aria-hidden responsive modification. +/// +/// - Parameter content: A closure containing style modifications for aria-hidden="true". +/// - Returns: A responsive modification for the aria-hidden state. +public func ariaHidden(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .ariaHidden, styleModification: content()) +} + +/// Creates an aria-pressed responsive modification. +/// +/// - Parameter content: A closure containing style modifications for aria-pressed="true". +/// - Returns: A responsive modification for the aria-pressed state. +public func ariaPressed(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .ariaPressed, styleModification: content()) +} + +/// Creates an aria-readonly responsive modification. +/// +/// - Parameter content: A closure containing style modifications for aria-readonly="true". +/// - Returns: A responsive modification for the aria-readonly state. +public func ariaReadonly(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .ariaReadonly, styleModification: content()) +} + +/// Creates an aria-required responsive modification. +/// +/// - Parameter content: A closure containing style modifications for aria-required="true". +/// - Returns: A responsive modification for the aria-required state. +public func ariaRequired(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .ariaRequired, styleModification: content()) +} + +/// Creates an aria-selected responsive modification. +/// +/// - Parameter content: A closure containing style modifications for aria-selected="true". +/// - Returns: A responsive modification for the aria-selected state. +public func ariaSelected(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .ariaSelected, styleModification: content()) +} \ No newline at end of file diff --git a/Sources/WebUI/Styles/Layout/Display/DisplayStyleOperation.swift b/Sources/WebUI/Styles/Layout/Display/DisplayStyleOperation.swift new file mode 100644 index 00000000..68bb9a32 --- /dev/null +++ b/Sources/WebUI/Styles/Layout/Display/DisplayStyleOperation.swift @@ -0,0 +1,103 @@ +import Foundation + +/// Style operation for display styling +/// +/// Provides a unified implementation for display styling that can be used across +/// Element methods and the Declarative DSL functions. +public struct DisplayStyleOperation: StyleOperation, @unchecked Sendable { + /// Parameters for display styling + public struct Parameters { + /// The display type + public let type: DisplayType + + /// Creates parameters for display styling + /// + /// - Parameter type: The display type + public init(type: DisplayType) { + self.type = type + } + + /// Creates parameters from a StyleParameters container + /// + /// - Parameter params: The style parameters container + /// - Returns: DisplayStyleOperation.Parameters + public static func from(_ params: StyleParameters) -> Parameters { + Parameters( + type: params.get("type")! + ) + } + } + + /// Applies the display style and returns the appropriate CSS classes + /// + /// - Parameter params: The parameters for display styling + /// - Returns: An array of CSS class names to be applied to elements + public func applyClasses(params: Parameters) -> [String] { + ["display-\(params.type.rawValue)"] + } + + /// Shared instance for use across the framework + public static let shared = DisplayStyleOperation() + + /// Private initializer to enforce singleton usage + private init() {} +} + +// Extension for Element to provide display styling +extension Element { + /// Sets the CSS display property with optional modifiers. + /// + /// - Parameters: + /// - type: The display type to apply. + /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. + /// - Returns: A new element with updated display classes. + /// + /// ## Example + /// ```swift + /// // Make an element a block + /// Element(tag: "span").display(.block) + /// + /// // Make an element inline-block on hover + /// Element(tag: "div").display(.inlineBlock, on: .hover) + /// + /// // Display as table on medium screens and up + /// Element(tag: "div").display(.table, on: .md) + /// ``` + public func display( + _ type: DisplayType, + on modifiers: Modifier... + ) -> Element { + let params = DisplayStyleOperation.Parameters(type: type) + + return DisplayStyleOperation.shared.applyToElement( + self, + params: params, + modifiers: modifiers + ) + } +} + +// Extension for ResponsiveBuilder to provide display styling +extension ResponsiveBuilder { + /// Sets the CSS display property in a responsive context. + /// + /// - Parameter type: The display type to apply. + /// - Returns: The builder for method chaining. + @discardableResult + public func display(_ type: DisplayType) -> ResponsiveBuilder { + let params = DisplayStyleOperation.Parameters(type: type) + + return DisplayStyleOperation.shared.applyToBuilder(self, params: params) + } +} + +// Global function for Declarative DSL +/// Sets the CSS display property in the responsive context. +/// +/// - Parameter type: The display type to apply. +/// - Returns: A responsive modification for display. +public func display(_ type: DisplayType) -> ResponsiveModification { + let params = DisplayStyleOperation.Parameters(type: type) + + return DisplayStyleOperation.shared.asModification(params: params) +} diff --git a/Sources/WebUI/Styles/Layout/Display/DisplayTypes.swift b/Sources/WebUI/Styles/Layout/Display/DisplayTypes.swift new file mode 100644 index 00000000..21e9d6e1 --- /dev/null +++ b/Sources/WebUI/Styles/Layout/Display/DisplayTypes.swift @@ -0,0 +1,88 @@ +/// Defines CSS display types for controlling element rendering. +/// +/// Specifies how an element is displayed in the layout. +public enum DisplayType: String { + /// Makes the element not display at all (removed from layout flow). + case none + /// Standard block element (takes full width, creates new line). + case block + /// Inline element (flows with text, no line breaks). + case inline + /// Hybrid that allows width/height but flows inline. + case inlineBlock = "inline-block" + /// Creates a flex container. + case flex + /// Creates an inline flex container. + case inlineFlex = "inline-flex" + /// Creates a grid container. + case grid + /// Creates an inline grid container. + case inlineGrid = "inline-grid" + /// Creates a table element. + case table + /// Creates a table cell element. + case tableCell = "table-cell" + /// Creates a table row element. + case tableRow = "table-row" +} + +/// Defines justification options for layout alignment. +/// +/// Specifies how items are distributed along the main axis in flexbox or grid layouts. +public enum Justify: String { + /// Aligns items to the start of the horizontal axis. + case start + /// Aligns items to the end of the horizontal axis. + case end + /// Centers items along the horizontal axis. + case center + /// Distributes items with equal space between them. + case between + /// Distributes items with equal space around them. + case around + /// Distributes items with equal space between and around them. + case evenly + + /// Provides the raw CSS class value. + public var rawValue: String { "justify-\(self)" } +} + +/// Represents alignment options for flexbox or grid items. +/// +/// Specifies how items are aligned along the secondary axis in flexbox or grid layouts. +public enum Align: String { + /// Aligns items to the start of the vertical axis + case start + /// Aligns items to the end of the vertical axis + case end + /// Centers items along the vertical axis + case center + /// Aligns items to their baseline + case baseline + /// Stretches items to fill the vertical axis + case stretch + + public var rawValue: String { "items-\(self)" } +} + +/// Represents flexbox direction options. +/// +/// Dictates the direction elements flow in a flexbox layout. +public enum Direction: String { + /// Sets the main axis to horizontal (left to right) + case row = "flex-row" + /// Sets the main axis to vertical (top to bottom) + case column = "flex-col" + /// Sets the main axis to horizontal (right to left) + case rowReverse = "flex-row-reverse" + /// Sets the main axis to vertical (bottom to top) + case colReverse = "flex-col-reverse" +} + +/// Represents a flex grow value; dictates whether the container should fill remaining space +public enum Grow: Int { + /// Indicates the container should not fill remaining space + case zero = 0 + /// Indicates the container should fill remaining space + case one = 1 +} diff --git a/Sources/WebUI/Styles/Layout/Display/FlexStyleOperation.swift b/Sources/WebUI/Styles/Layout/Display/FlexStyleOperation.swift new file mode 100644 index 00000000..f40e74bd --- /dev/null +++ b/Sources/WebUI/Styles/Layout/Display/FlexStyleOperation.swift @@ -0,0 +1,259 @@ +import Foundation + +/// Style operation for flex container styling +/// +/// Provides a unified implementation for flex styling that can be used across +/// Element methods and the Declarative DSL functions. +public struct FlexStyleOperation: StyleOperation, @unchecked Sendable { + /// Parameters for flex styling + public struct Parameters { + /// The flex direction (row, column, etc.) + public let direction: FlexDirection? + + /// The justify content property (start, center, between, etc.) + public let justify: FlexJustify? + + /// The align items property (start, center, end, etc.) + public let align: FlexAlign? + + /// The flex grow property + public let grow: FlexGrow? + + /// Creates parameters for flex styling + /// + /// - Parameters: + /// - direction: The flex direction + /// - justify: The justify content property + /// - align: The align items property + /// - grow: The flex grow property + public init( + direction: FlexDirection? = nil, + justify: FlexJustify? = nil, + align: FlexAlign? = nil, + grow: FlexGrow? = nil + ) { + self.direction = direction + self.justify = justify + self.align = align + self.grow = grow + } + + /// Creates parameters from a StyleParameters container + /// + /// - Parameter params: The style parameters container + /// - Returns: FlexStyleOperation.Parameters + public static func from(_ params: StyleParameters) -> Parameters { + Parameters( + direction: params.get("direction"), + justify: params.get("justify"), + align: params.get("align"), + grow: params.get("grow") + ) + } + } + + /// Applies the flex style and returns the appropriate CSS classes + /// + /// - Parameter params: The parameters for flex styling + /// - Returns: An array of CSS class names to be applied to elements + public func applyClasses(params: Parameters) -> [String] { + var classes = ["flex"] + + if let direction = params.direction { + classes.append("flex-\(direction.rawValue)") + } + + if let justify = params.justify { + classes.append("justify-\(justify.rawValue)") + } + + if let align = params.align { + classes.append("items-\(align.rawValue)") + } + + if let grow = params.grow { + classes.append("flex-\(grow.rawValue)") + } + + return classes + } + + /// Shared instance for use across the framework + public static let shared = FlexStyleOperation() + + /// Private initializer to enforce singleton usage + private init() {} +} + +/// Defines the flex direction +public enum FlexDirection: String { + /// Items are arranged horizontally (left to right) + case row + + /// Items are arranged horizontally in reverse (right to left) + case rowReverse = "row-reverse" + + /// Items are arranged vertically (top to bottom) + case column = "col" + + /// Items are arranged vertically in reverse (bottom to top) + case columnReverse = "col-reverse" +} + +/// Defines how flex items are justified along the main axis +public enum FlexJustify: String { + /// Items are packed at the start of the container + case start + + /// Items are packed at the end of the container + case end + + /// Items are centered along the line + case center + + /// Items are evenly distributed with equal space between them + case between + + /// Items are evenly distributed with equal space around them + case around + + /// Items are evenly distributed with equal space between and around them + case evenly +} + +/// Defines how flex items are aligned along the cross axis +public enum FlexAlign: String { + /// Items are aligned at the start of the cross axis + case start + + /// Items are aligned at the end of the cross axis + case end + + /// Items are centered along the cross axis + case center + + /// Items are stretched to fill the container + case stretch + + /// Items are aligned at the baseline + case baseline +} + +/// Defines the flex grow factor +public enum FlexGrow: String { + /// No growing + case none = "0" + + /// Grow with factor 1 + case one = "1" + + /// Grow with factor 2 + case two = "2" + + /// Grow with factor 3 + case three = "3" + + /// Grow with factor 4 + case four = "4" + + /// Grow with factor 5 + case five = "5" +} + +// Extension for Element to provide flex styling +extension Element { + /// Sets flex container properties with optional modifiers. + /// + /// - Parameters: + /// - direction: The flex direction (row, column, etc.). + /// - justify: How to align items along the main axis. + /// - align: How to align items along the cross axis. + /// - grow: The flex grow factor. + /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. + /// - Returns: A new element with updated flex classes. + /// + /// ## Example + /// ```swift + /// // Create a flex container with column layout + /// Element(tag: "div").flex(direction: .column) + /// + /// // Create a flex container with row layout and centered content + /// Element(tag: "div").flex(direction: .row, justify: .center, align: .center) + /// + /// // Apply flex layout only on medium screens and up + /// Element(tag: "div").flex(direction: .row, on: .md) + /// ``` + public func flex( + direction: FlexDirection? = nil, + justify: FlexJustify? = nil, + align: FlexAlign? = nil, + grow: FlexGrow? = nil, + on modifiers: Modifier... + ) -> Element { + let params = FlexStyleOperation.Parameters( + direction: direction, + justify: justify, + align: align, + grow: grow + ) + + return FlexStyleOperation.shared.applyToElement( + self, + params: params, + modifiers: modifiers + ) + } +} + +// Extension for ResponsiveBuilder to provide flex styling +extension ResponsiveBuilder { + /// Sets flex container properties in a responsive context. + /// + /// - Parameters: + /// - direction: The flex direction (row, column, etc.). + /// - justify: How to align items along the main axis. + /// - align: How to align items along the cross axis. + /// - grow: The flex grow factor. + /// - Returns: The builder for method chaining. + @discardableResult + public func flex( + direction: FlexDirection? = nil, + justify: FlexJustify? = nil, + align: FlexAlign? = nil, + grow: FlexGrow? = nil + ) -> ResponsiveBuilder { + let params = FlexStyleOperation.Parameters( + direction: direction, + justify: justify, + align: align, + grow: grow + ) + + return FlexStyleOperation.shared.applyToBuilder(self, params: params) + } +} + +// Global function for Declarative DSL +/// Sets flex container properties in the responsive context. +/// +/// - Parameters: +/// - direction: The flex direction (row, column, etc.). +/// - justify: How to align items along the main axis. +/// - align: How to align items along the cross axis. +/// - grow: The flex grow factor. +/// - Returns: A responsive modification for flex container. +public func flex( + direction: FlexDirection? = nil, + justify: FlexJustify? = nil, + align: FlexAlign? = nil, + grow: FlexGrow? = nil +) -> ResponsiveModification { + let params = FlexStyleOperation.Parameters( + direction: direction, + justify: justify, + align: align, + grow: grow + ) + + return FlexStyleOperation.shared.asModification(params: params) +} \ No newline at end of file diff --git a/Sources/WebUI/Styles/Layout/Display/GridStyleOperation.swift b/Sources/WebUI/Styles/Layout/Display/GridStyleOperation.swift new file mode 100644 index 00000000..10a2d4c4 --- /dev/null +++ b/Sources/WebUI/Styles/Layout/Display/GridStyleOperation.swift @@ -0,0 +1,219 @@ +import Foundation + +/// Style operation for grid container styling +/// +/// Provides a unified implementation for grid styling that can be used across +/// Element methods and the Declarative DSL functions. +public struct GridStyleOperation: StyleOperation, @unchecked Sendable { + /// Parameters for grid styling + public struct Parameters { + /// The number of grid columns + public let columns: Int? + + /// The number of grid rows + public let rows: Int? + + /// The grid flow direction + public let flow: GridFlow? + + /// The column span value + public let columnSpan: Int? + + /// The row span value + public let rowSpan: Int? + + /// Creates parameters for grid styling + /// + /// - Parameters: + /// - columns: The number of grid columns + /// - rows: The number of grid rows + /// - flow: The grid flow direction + /// - columnSpan: The column span value + /// - rowSpan: The row span value + public init( + columns: Int? = nil, + rows: Int? = nil, + flow: GridFlow? = nil, + columnSpan: Int? = nil, + rowSpan: Int? = nil + ) { + self.columns = columns + self.rows = rows + self.flow = flow + self.columnSpan = columnSpan + self.rowSpan = rowSpan + } + + /// Creates parameters from a StyleParameters container + /// + /// - Parameter params: The style parameters container + /// - Returns: GridStyleOperation.Parameters + public static func from(_ params: StyleParameters) -> Parameters { + Parameters( + columns: params.get("columns"), + rows: params.get("rows"), + flow: params.get("flow"), + columnSpan: params.get("columnSpan"), + rowSpan: params.get("rowSpan") + ) + } + } + + /// Applies the grid style and returns the appropriate CSS classes + /// + /// - Parameter params: The parameters for grid styling + /// - Returns: An array of CSS class names to be applied to elements + public func applyClasses(params: Parameters) -> [String] { + var classes = ["grid"] + + if let columns = params.columns { + classes.append("grid-cols-\(columns)") + } + + if let rows = params.rows { + classes.append("grid-rows-\(rows)") + } + + if let flow = params.flow { + classes.append("grid-flow-\(flow.rawValue)") + } + + if let columnSpan = params.columnSpan { + classes.append("col-span-\(columnSpan)") + } + + if let rowSpan = params.rowSpan { + classes.append("row-span-\(rowSpan)") + } + + return classes + } + + /// Shared instance for use across the framework + public static let shared = GridStyleOperation() + + /// Private initializer to enforce singleton usage + private init() {} +} + +/// Defines the grid flow direction +public enum GridFlow: String { + /// Items flow row by row + case row + + /// Items flow column by column + case col + + /// Items flow row by row, dense packing + case rowDense = "row-dense" + + /// Items flow column by column, dense packing + case colDense = "col-dense" +} + +// Extension for Element to provide grid styling +extension Element { + /// Sets grid container properties with optional modifiers. + /// + /// - Parameters: + /// - columns: The number of grid columns. + /// - rows: The number of grid rows. + /// - flow: The grid flow direction. + /// - columnSpan: The column span value. + /// - rowSpan: The row span value. + /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. + /// - Returns: A new element with updated grid classes. + /// + /// ## Example + /// ```swift + /// // Create a grid container with 3 columns + /// Element(tag: "div").grid(columns: 3) + /// + /// // Create a grid container with 2 columns and 3 rows + /// Element(tag: "div").grid(columns: 2, rows: 3) + /// + /// // Apply grid layout only on medium screens and up + /// Element(tag: "div").grid(columns: 2, on: .md) + /// ``` + public func grid( + columns: Int? = nil, + rows: Int? = nil, + flow: GridFlow? = nil, + columnSpan: Int? = nil, + rowSpan: Int? = nil, + on modifiers: Modifier... + ) -> Element { + let params = GridStyleOperation.Parameters( + columns: columns, + rows: rows, + flow: flow, + columnSpan: columnSpan, + rowSpan: rowSpan + ) + + return GridStyleOperation.shared.applyToElement( + self, + params: params, + modifiers: modifiers + ) + } +} + +// Extension for ResponsiveBuilder to provide grid styling +extension ResponsiveBuilder { + /// Sets grid container properties in a responsive context. + /// + /// - Parameters: + /// - columns: The number of grid columns. + /// - rows: The number of grid rows. + /// - flow: The grid flow direction. + /// - columnSpan: The column span value. + /// - rowSpan: The row span value. + /// - Returns: The builder for method chaining. + @discardableResult + public func grid( + columns: Int? = nil, + rows: Int? = nil, + flow: GridFlow? = nil, + columnSpan: Int? = nil, + rowSpan: Int? = nil + ) -> ResponsiveBuilder { + let params = GridStyleOperation.Parameters( + columns: columns, + rows: rows, + flow: flow, + columnSpan: columnSpan, + rowSpan: rowSpan + ) + + return GridStyleOperation.shared.applyToBuilder(self, params: params) + } +} + +// Global function for Declarative DSL +/// Sets grid container properties in the responsive context. +/// +/// - Parameters: +/// - columns: The number of grid columns. +/// - rows: The number of grid rows. +/// - flow: The grid flow direction. +/// - columnSpan: The column span value. +/// - rowSpan: The row span value. +/// - Returns: A responsive modification for grid container. +public func grid( + columns: Int? = nil, + rows: Int? = nil, + flow: GridFlow? = nil, + columnSpan: Int? = nil, + rowSpan: Int? = nil +) -> ResponsiveModification { + let params = GridStyleOperation.Parameters( + columns: columns, + rows: rows, + flow: flow, + columnSpan: columnSpan, + rowSpan: rowSpan + ) + + return GridStyleOperation.shared.asModification(params: params) +} \ No newline at end of file diff --git a/Sources/WebUI/Styles/Layout/Display/VisibilityStyleOperation.swift b/Sources/WebUI/Styles/Layout/Display/VisibilityStyleOperation.swift new file mode 100644 index 00000000..919f01fd --- /dev/null +++ b/Sources/WebUI/Styles/Layout/Display/VisibilityStyleOperation.swift @@ -0,0 +1,110 @@ +import Foundation + +/// Style operation for visibility styling +/// +/// Provides a unified implementation for visibility styling that can be used across +/// Element methods and the Declarative DSL functions. +public struct VisibilityStyleOperation: StyleOperation, @unchecked Sendable { + /// Parameters for visibility styling + public struct Parameters { + /// Whether the element should be hidden + public let isHidden: Bool + + /// Creates parameters for visibility styling + /// + /// - Parameter isHidden: Whether the element should be hidden + public init(isHidden: Bool = true) { + self.isHidden = isHidden + } + + /// Creates parameters from a StyleParameters container + /// + /// - Parameter params: The style parameters container + /// - Returns: VisibilityStyleOperation.Parameters + public static func from(_ params: StyleParameters) -> Parameters { + Parameters( + isHidden: params.get("isHidden") ?? true + ) + } + } + + /// Applies the visibility style and returns the appropriate CSS classes + /// + /// - Parameter params: The parameters for visibility styling + /// - Returns: An array of CSS class names to be applied to elements + public func applyClasses(params: Parameters) -> [String] { + if params.isHidden { + return ["hidden"] + } else { + return [] + } + } + + /// Shared instance for use across the framework + public static let shared = VisibilityStyleOperation() + + /// Private initializer to enforce singleton usage + private init() {} +} + +// Extension for Element to provide visibility styling +extension Element { + /// Controls the visibility of an element with optional modifiers. + /// + /// - Parameters: + /// - isHidden: Whether the element should be hidden (default: true). + /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. + /// - Returns: A new element with updated visibility classes. + /// + /// ## Example + /// ```swift + /// // Hide an element + /// Element(tag: "div").hidden() + /// + /// // Show an element on hover + /// Element(tag: "div").hidden(on: .hover) + /// + /// // Hide an element on medium screens and up + /// Element(tag: "div").hidden(on: .md) + /// + /// // Make visible on medium screens + /// Element(tag: "div").hidden(false, on: .md) + /// ``` + public func hidden( + _ isHidden: Bool = true, + on modifiers: Modifier... + ) -> Element { + let params = VisibilityStyleOperation.Parameters(isHidden: isHidden) + + return VisibilityStyleOperation.shared.applyToElement( + self, + params: params, + modifiers: modifiers + ) + } +} + +// Extension for ResponsiveBuilder to provide visibility styling +extension ResponsiveBuilder { + /// Controls the visibility of an element in a responsive context. + /// + /// - Parameter isHidden: Whether the element should be hidden (default: true). + /// - Returns: The builder for method chaining. + @discardableResult + public func hidden(_ isHidden: Bool = true) -> ResponsiveBuilder { + let params = VisibilityStyleOperation.Parameters(isHidden: isHidden) + + return VisibilityStyleOperation.shared.applyToBuilder(self, params: params) + } +} + +// Global function for Declarative DSL +/// Controls the visibility of an element in the responsive context. +/// +/// - Parameter isHidden: Whether the element should be hidden (default: true). +/// - Returns: A responsive modification for visibility. +public func hidden(_ isHidden: Bool = true) -> ResponsiveModification { + let params = VisibilityStyleOperation.Parameters(isHidden: isHidden) + + return VisibilityStyleOperation.shared.asModification(params: params) +} \ No newline at end of file diff --git a/Sources/WebUI/Styles/Base/MarginsStyleOperation.swift b/Sources/WebUI/Styles/Layout/MarginsStyleOperation.swift similarity index 100% rename from Sources/WebUI/Styles/Base/MarginsStyleOperation.swift rename to Sources/WebUI/Styles/Layout/MarginsStyleOperation.swift diff --git a/Sources/WebUI/Styles/Positioning/Overflow/OverflowStyleOperation.swift b/Sources/WebUI/Styles/Layout/Overflow/OverflowStyleOperation.swift similarity index 100% rename from Sources/WebUI/Styles/Positioning/Overflow/OverflowStyleOperation.swift rename to Sources/WebUI/Styles/Layout/Overflow/OverflowStyleOperation.swift diff --git a/Sources/WebUI/Styles/Positioning/Overflow/OverflowTypes.swift b/Sources/WebUI/Styles/Layout/Overflow/OverflowTypes.swift similarity index 100% rename from Sources/WebUI/Styles/Positioning/Overflow/OverflowTypes.swift rename to Sources/WebUI/Styles/Layout/Overflow/OverflowTypes.swift diff --git a/Sources/WebUI/Styles/Base/PaddingStyleOperation.swift b/Sources/WebUI/Styles/Layout/PaddingStyleOperation.swift similarity index 100% rename from Sources/WebUI/Styles/Base/PaddingStyleOperation.swift rename to Sources/WebUI/Styles/Layout/PaddingStyleOperation.swift diff --git a/Sources/WebUI/Styles/Positioning/Position/PositionStyleOperation.swift b/Sources/WebUI/Styles/Layout/Position/PositionStyleOperation.swift similarity index 100% rename from Sources/WebUI/Styles/Positioning/Position/PositionStyleOperation.swift rename to Sources/WebUI/Styles/Layout/Position/PositionStyleOperation.swift diff --git a/Sources/WebUI/Styles/Positioning/Position/PositionTypes.swift b/Sources/WebUI/Styles/Layout/Position/PositionTypes.swift similarity index 100% rename from Sources/WebUI/Styles/Positioning/Position/PositionTypes.swift rename to Sources/WebUI/Styles/Layout/Position/PositionTypes.swift diff --git a/Sources/WebUI/Styles/Positioning/Scroll/ScrollStyleOperation.swift b/Sources/WebUI/Styles/Layout/Scroll/ScrollStyleOperation.swift similarity index 100% rename from Sources/WebUI/Styles/Positioning/Scroll/ScrollStyleOperation.swift rename to Sources/WebUI/Styles/Layout/Scroll/ScrollStyleOperation.swift diff --git a/Sources/WebUI/Styles/Positioning/Scroll/ScrollTypes.swift b/Sources/WebUI/Styles/Layout/Scroll/ScrollTypes.swift similarity index 100% rename from Sources/WebUI/Styles/Positioning/Scroll/ScrollTypes.swift rename to Sources/WebUI/Styles/Layout/Scroll/ScrollTypes.swift diff --git a/Sources/WebUI/Styles/Base/Sizing/SizingStyleOperation.swift b/Sources/WebUI/Styles/Layout/Sizing/SizingStyleOperation.swift similarity index 100% rename from Sources/WebUI/Styles/Base/Sizing/SizingStyleOperation.swift rename to Sources/WebUI/Styles/Layout/Sizing/SizingStyleOperation.swift diff --git a/Sources/WebUI/Styles/Base/Sizing/SizingTypes.swift b/Sources/WebUI/Styles/Layout/Sizing/SizingTypes.swift similarity index 100% rename from Sources/WebUI/Styles/Base/Sizing/SizingTypes.swift rename to Sources/WebUI/Styles/Layout/Sizing/SizingTypes.swift diff --git a/Sources/WebUI/Styles/Base/SpacingStyleOperation.swift b/Sources/WebUI/Styles/Layout/SpacingStyleOperation.swift similarity index 100% rename from Sources/WebUI/Styles/Base/SpacingStyleOperation.swift rename to Sources/WebUI/Styles/Layout/SpacingStyleOperation.swift diff --git a/Sources/WebUI/Styles/Positioning/ZIndexStyleOperation.swift b/Sources/WebUI/Styles/Layout/ZIndexStyleOperation.swift similarity index 100% rename from Sources/WebUI/Styles/Positioning/ZIndexStyleOperation.swift rename to Sources/WebUI/Styles/Layout/ZIndexStyleOperation.swift diff --git a/Sources/WebUI/Styles/Responsive/ResponsiveAlias.swift b/Sources/WebUI/Styles/Responsive/ResponsiveAlias.swift new file mode 100644 index 00000000..2c0972b1 --- /dev/null +++ b/Sources/WebUI/Styles/Responsive/ResponsiveAlias.swift @@ -0,0 +1,15 @@ +import Foundation + +/// Provides a backward compatibility alias for the new responsive styling API +extension Element { + /// Alias for the `.on` method to maintain backward compatibility with existing code + /// + /// This method provides a backward-compatible way to use the new `.on` method + /// with code that was written to use `.responsive`. + /// + /// - Parameter content: A closure defining responsive style configurations using the result builder. + /// - Returns: An element with responsive styles applied. + public func responsive(@ResponsiveStyleBuilder _ content: () -> ResponsiveModification) -> Element { + return on(content) + } +} \ No newline at end of file diff --git a/Sources/WebUI/Styles/Responsive/ResponsiveModifier.swift b/Sources/WebUI/Styles/Responsive/ResponsiveModifier.swift new file mode 100644 index 00000000..014bc1a5 --- /dev/null +++ b/Sources/WebUI/Styles/Responsive/ResponsiveModifier.swift @@ -0,0 +1,225 @@ +import Foundation + +/// Provides a block-based responsive design API for WebUI elements. +/// +/// The on modifier allows developers to define responsive styles +/// across different breakpoints in a single, concise block, creating cleaner +/// and more maintainable code for responsive designs. +/// +/// ## Example +/// ```swift +/// Text { "Responsive Content" } +/// .font(size: .sm) +/// .background(color: .neutral(._500)) +/// .on { +/// md { +/// font(size: .lg) +/// background(color: .neutral(._700)) +/// padding(of: 4) +/// } +/// lg { +/// font(size: .xl) +/// background(color: .neutral(._900)) +/// font(alignment: .center) +/// } +/// } +/// ``` +extension Element { + /// Applies responsive styling across different breakpoints with a declarative syntax. + /// + /// This method provides a clean, declarative way to define styles for multiple + /// breakpoints in a single block, improving code readability and maintainability. + /// + /// - Parameter content: A closure defining responsive style configurations using the result builder. + /// - Returns: An element with responsive styles applied. + /// + /// ## Example + /// ```swift + /// Button { "Submit" } + /// .background(color: .blue(._500)) + /// .on { + /// sm { + /// padding(of: 2) + /// font(size: .sm) + /// } + /// md { + /// padding(of: 4) + /// font(size: .base) + /// } + /// lg { + /// padding(of: 6) + /// font(size: .lg) + /// } + /// } + /// ``` + public func on(@ResponsiveStyleBuilder _ content: () -> ResponsiveModification) -> Element { + let builder = ResponsiveBuilder(element: self) + let modification = content() + modification.apply(to: builder) + return builder.element + } +} + +/// Builds responsive style configurations for elements across different breakpoints. +/// +/// `ResponsiveBuilder` provides a fluent, method-chaining API for applying style +/// modifications at specific screen sizes. Each method represents a breakpoint +/// and accepts a closure where style modifications can be defined. +/// +/// This class is not typically created directly, but instead used through the +/// `Element.on(_:)` method. +public class ResponsiveBuilder { + /// The current element being modified + var element: Element + /// Keep track of responsive styles for each breakpoint + internal var pendingClasses: [String] = [] + /// The current breakpoint being modified + internal var currentBreakpoint: Modifier? + + /// Creates a new responsive builder for the given element. + /// + /// - Parameter element: The element to apply responsive styles to. + init(element: Element) { + self.element = element + } + + /// Applies styles at the extra-small breakpoint (480px+). + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func xs(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .xs + modifications(self) + applyBreakpoint() + return self + } + + /// Applies styles at the small breakpoint (640px+). + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func sm(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .sm + modifications(self) + applyBreakpoint() + return self + } + + /// Applies styles at the medium breakpoint (768px+). + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func md(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .md + modifications(self) + applyBreakpoint() + return self + } + + /// Applies styles at the large breakpoint (1024px+). + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func lg(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .lg + modifications(self) + applyBreakpoint() + return self + } + + /// Applies styles at the extra-large breakpoint (1280px+). + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func xl(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .xl + modifications(self) + applyBreakpoint() + return self + } + + /// Applies styles at the 2x-extra-large breakpoint (1536px+). + /// + /// - Parameter modifications: A closure containing style modifications. + /// - Returns: The builder for method chaining. + @discardableResult + public func xl2(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { + currentBreakpoint = .xl2 + modifications(self) + applyBreakpoint() + return self + } + + /// Applies the breakpoint prefix to all pending classes and add them to the element + internal func applyBreakpoint() { + guard let breakpoint = currentBreakpoint else { return } + + // Apply the breakpoint prefix to all pending classes + let responsiveClasses = pendingClasses.map { + // Handle duplication for flex-*, justify-*, items-* + if $0.starts(with: "flex-") || $0.starts(with: "justify-") || $0.starts(with: "items-") + || $0.starts(with: "grid-") + { + return "\(breakpoint.rawValue)\($0)" + } else if $0 == "flex" || $0 == "grid" { + return "\(breakpoint.rawValue)\($0)" + } else { + return "\(breakpoint.rawValue)\($0)" + } + } + + // Add the responsive classes to the element + self.element = Element( + tag: self.element.tag, + id: self.element.id, + classes: (self.element.classes ?? []) + responsiveClasses, + role: self.element.role, + label: self.element.label, + data: self.element.data, + isSelfClosing: self.element.isSelfClosing, + customAttributes: self.element.customAttributes, + content: self.element.contentBuilder + ) + + // Clear pending classes for the next breakpoint + pendingClasses = [] + currentBreakpoint = nil + } + + /// Add a class to the pending list of classes + public func addClass(_ className: String) { + pendingClasses.append(className) + } +} + +// Font styling methods +extension ResponsiveBuilder { + @discardableResult + public func size(_ value: Int) -> ResponsiveBuilder { + addClass("size-\(value)") + return self + } + + @discardableResult + public func frame( + width: Int? = nil, + height: Int? = nil, + minWidth: Int? = nil, + maxWidth: Int? = nil, + minHeight: Int? = nil, + maxHeight: Int? = nil + ) -> ResponsiveBuilder { + if let width = width { addClass("w-\(width)") } + if let height = height { addClass("h-\(height)") } + if let minWidth = minWidth { addClass("min-w-\(minWidth)") } + if let maxWidth = maxWidth { addClass("max-w-\(maxWidth)") } + if let minHeight = minHeight { addClass("min-h-\(minHeight)") } + if let maxHeight = maxHeight { addClass("max-h-\(maxHeight)") } + return self + } +} diff --git a/Sources/WebUI/Styles/Responsive/ResponsiveStyleBuilder.swift b/Sources/WebUI/Styles/Responsive/ResponsiveStyleBuilder.swift new file mode 100644 index 00000000..9ca7e177 --- /dev/null +++ b/Sources/WebUI/Styles/Responsive/ResponsiveStyleBuilder.swift @@ -0,0 +1,75 @@ +import Foundation + +/// A result builder for creating responsive styles with a clean, SwiftUI-like syntax. +/// +/// This builder enables a more natural way to define responsive styles without using `$0` references. +/// +/// ## Example +/// ```swift +/// Element(tag: "div") +/// .responsive { +/// sm { +/// font(size: .base) +/// } +/// md { +/// font(size: .lg) +/// background(color: .blue(._500)) +/// } +/// } +/// ``` +@resultBuilder +public struct ResponsiveStyleBuilder { + /// Builds an empty responsive style. + public static func buildBlock() -> ResponsiveModification { + EmptyResponsiveModification() + } + + /// Builds a responsive style from multiple modifications. + public static func buildBlock(_ components: ResponsiveModification...) -> ResponsiveModification { + CompositeResponsiveModification(modifications: components) + } + + /// Transforms an optional into a responsive modification. + public static func buildOptional(_ component: ResponsiveModification?) -> ResponsiveModification { + component ?? EmptyResponsiveModification() + } + + /// Transforms an either-or condition into a responsive modification. + public static func buildEither(first component: ResponsiveModification) -> ResponsiveModification { + component + } + + /// Transforms an either-or condition into a responsive modification. + public static func buildEither(second component: ResponsiveModification) -> ResponsiveModification { + component + } + + /// Transforms an array of responsive modifications into a single modification. + public static func buildArray(_ components: [ResponsiveModification]) -> ResponsiveModification { + CompositeResponsiveModification(modifications: components) + } +} + +/// Protocol defining the interface for responsive style modifications. +public protocol ResponsiveModification { + /// Applies the modification to the given responsive builder. + func apply(to builder: ResponsiveBuilder) +} + +/// Represents an empty responsive modification. +struct EmptyResponsiveModification: ResponsiveModification { + func apply(to builder: ResponsiveBuilder) { + // Do nothing for empty modifications + } +} + +/// Represents a composite of multiple responsive modifications. +struct CompositeResponsiveModification: ResponsiveModification { + let modifications: [ResponsiveModification] + + func apply(to builder: ResponsiveBuilder) { + for modification in modifications { + modification.apply(to: builder) + } + } +} diff --git a/Sources/WebUI/Styles/Responsive/ResponsiveStyleModifiers.swift b/Sources/WebUI/Styles/Responsive/ResponsiveStyleModifiers.swift new file mode 100644 index 00000000..d0d9b45d --- /dev/null +++ b/Sources/WebUI/Styles/Responsive/ResponsiveStyleModifiers.swift @@ -0,0 +1,214 @@ +import Foundation + +/// Provides the implementation for breakpoint and interactive state modifiers in the responsive DSL. +/// +/// These functions are available in the context of a responsive closure, allowing +/// for a more natural, SwiftUI-like syntax without requiring `$0` references. +public struct BreakpointModification: ResponsiveModification { + private let breakpoint: Modifier + private let styleModification: ResponsiveModification + + init(breakpoint: Modifier, styleModification: ResponsiveModification) { + self.breakpoint = breakpoint + self.styleModification = styleModification + } + + public func apply(to builder: ResponsiveBuilder) { + switch breakpoint { + case .xs: + builder.xs { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .sm: + builder.sm { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .md: + builder.md { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .lg: + builder.lg { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .xl: + builder.xl { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .xl2: + builder.xl2 { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .hover: + builder.hover { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .focus: + builder.focus { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .active: + builder.active { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .placeholder: + builder.placeholder { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .dark: + builder.dark { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .first: + builder.first { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .last: + builder.last { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .disabled: + builder.disabled { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .motionReduce: + builder.motionReduce { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .ariaBusy: + builder.ariaBusy { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .ariaChecked: + builder.ariaChecked { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .ariaDisabled: + builder.ariaDisabled { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .ariaExpanded: + builder.ariaExpanded { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .ariaHidden: + builder.ariaHidden { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .ariaPressed: + builder.ariaPressed { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .ariaReadonly: + builder.ariaReadonly { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .ariaRequired: + builder.ariaRequired { innerBuilder in + styleModification.apply(to: innerBuilder) + } + case .ariaSelected: + builder.ariaSelected { innerBuilder in + styleModification.apply(to: innerBuilder) + } + } + } +} + +/// Represents a style modification in the responsive DSL. +public struct StyleModification: ResponsiveModification { + private let modification: (ResponsiveBuilder) -> Void + + init(_ modification: @escaping (ResponsiveBuilder) -> Void) { + self.modification = modification + } + + public func apply(to builder: ResponsiveBuilder) { + modification(builder) + } +} + +// MARK: - Breakpoint Functions +// Note: Interactive state functions like hover, focus, etc. are defined in InteractionModifiers.swift + +/// Creates an extra-small breakpoint (480px+) responsive modification. +/// +/// - Parameter content: A closure containing style modifications for this breakpoint. +/// - Returns: A responsive modification for the extra-small breakpoint. +public func xs(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .xs, styleModification: content()) +} + +/// Creates a small breakpoint (640px+) responsive modification. +/// +/// - Parameter content: A closure containing style modifications for this breakpoint. +/// - Returns: A responsive modification for the small breakpoint. +public func sm(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .sm, styleModification: content()) +} + +/// Creates a medium breakpoint (768px+) responsive modification. +/// +/// - Parameter content: A closure containing style modifications for this breakpoint. +/// - Returns: A responsive modification for the medium breakpoint. +public func md(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .md, styleModification: content()) +} + +/// Creates a large breakpoint (1024px+) responsive modification. +/// +/// - Parameter content: A closure containing style modifications for this breakpoint. +/// - Returns: A responsive modification for the large breakpoint. +public func lg(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .lg, styleModification: content()) +} + +/// Creates an extra-large breakpoint (1280px+) responsive modification. +/// +/// - Parameter content: A closure containing style modifications for this breakpoint. +/// - Returns: A responsive modification for the extra-large breakpoint. +public func xl(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .xl, styleModification: content()) +} + +/// Creates a 2x extra-large breakpoint (1536px+) responsive modification. +/// +/// - Parameter content: A closure containing style modifications for this breakpoint. +/// - Returns: A responsive modification for the 2x extra-large breakpoint. +public func xl2(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { + BreakpointModification(breakpoint: .xl2, styleModification: content()) +} + +// MARK: - Style Modification Functions + +// Font styling is now implemented in FontStyleOperation.swift + +// Background styling is now implemented in BackgroundStyleOperation.swift + +// Padding styling is now implemented in PaddingStyleOperation.swift + +// Margins styling is now implemented in MarginsStyleOperation.swift + +// Border styling is now implemented in BorderStyleOperation.swift + +// Opacity styling is now implemented in OpacityStyleOperation.swift + +// Size styling is now implemented in SizingStyleOperation.swift + +// Frame styling is now implemented in SizingStyleOperation.swift + +// Flex styling is implemented in ResponsiveBuilder.swift + +// Grid styling is implemented in ResponsiveBuilder.swift + +// Position styling is now implemented in PositionStyleOperation.swift + +// Overflow styling is now implemented in OverflowStyleOperation.swift + +// Hidden styling is implemented in ResponsiveBuilder.swift + +// Border radius styling is now implemented in BorderRadiusStyleOperation.swift + +// Interactive state modifiers like hover, focus, etc. are implemented in InteractionModifiers.swift + +// ARIA state modifiers are implemented in InteractionModifiers.swift diff --git a/Sources/WebUI/Styles/Base/Font/FontStyleOperation.swift b/Sources/WebUI/Styles/Typography/Font/FontStyleOperation.swift similarity index 100% rename from Sources/WebUI/Styles/Base/Font/FontStyleOperation.swift rename to Sources/WebUI/Styles/Typography/Font/FontStyleOperation.swift diff --git a/Sources/WebUI/Styles/Base/Font/FontTypes.swift b/Sources/WebUI/Styles/Typography/Font/FontTypes.swift similarity index 100% rename from Sources/WebUI/Styles/Base/Font/FontTypes.swift rename to Sources/WebUI/Styles/Typography/Font/FontTypes.swift From 013e5e30b30ef39de72a9c3a6ac3c6730de21636 Mon Sep 17 00:00:00 2001 From: Mac Date: Fri, 23 May 2025 13:12:06 +0100 Subject: [PATCH 4/5] chore: refactor because files duplicated on merge --- Sources/WebUI/Elements/Base/List.swift | 147 ------ Sources/WebUI/Elements/Base/Media.swift | 499 ------------------ .../{Base => Interactive}/Button.swift | 0 .../{ => Interactive}/Form/Form.swift | 0 .../Form/Input}/Input.swift | 0 .../Form/Input}/Label.swift | 0 .../{ => Interactive}/Form/TextArea.swift | 0 .../{Form => Interactive}/Progress.swift | 0 Sources/WebUI/Elements/Layout/Layout.swift | 273 ---------- Sources/WebUI/Elements/Layout/Structure.swift | 162 ------ Sources/WebUI/Elements/Progress.swift | 60 --- .../WebUI/Elements/Structure/Fragment.swift | 43 -- .../Layout/{Main.swift => MainElement.swift} | 4 +- .../WebUI/Elements/Structure/List/List.swift | 62 +++ .../{Base => Text}/Abbreviation.swift | 0 .../Elements/{Base => Text}/Emphasis.swift | 0 .../WebUI/Elements/{Base => Text}/Link.swift | 0 .../{Base => Text}/Preformatted.swift | 0 .../Elements/{Base => Text}/Strong.swift | 0 .../WebUI/Elements/{Base => Text}/Style.swift | 0 .../WebUI/Elements/{Base => Text}/Text.swift | 0 .../WebUI/Elements/{Base => Text}/Time.swift | 0 .../Appearance/BackgroundStyleOperation.swift | 107 ---- .../Display/DisplayStyleOperation.swift | 103 ---- .../Appearance/Display/DisplayTypes.swift | 88 --- .../Display/FlexStyleOperation.swift | 259 --------- .../Display/GridStyleOperation.swift | 219 -------- .../Display/VisibilityStyleOperation.swift | 110 ---- .../Appearance/OpacityStyleOperation.swift | 98 ---- .../Appearance/OutlineStyleOperation.swift | 188 ------- .../Shadow/ShadowStyleOperation.swift | 155 ------ .../Appearance/Shadow/ShadowTypes.swift | 27 - .../Color/Border/BorderStyleOperation.swift | 213 -------- .../Styles/Color/Border/BorderTypes.swift | 85 --- .../Styles/Color/RingStyleOperation.swift | 160 ------ .../Styles/Core/InteractionModifiers.swift | 370 ------------- .../WebUI/Styles/Core/ResponsiveAlias.swift | 15 - .../Styles/Core/ResponsiveModifier.swift | 225 -------- .../Styles/Core/ResponsiveStyleBuilder.swift | 75 --- .../Core/ResponsiveStyleModifiers.swift | 214 -------- .../Border}/BorderRadiusStyleOperation.swift | 0 .../Border/BorderStyleOperation.swift | 0 .../Border/BorderTypes.swift | 0 .../Effects/BorderRadiusStyleOperation.swift | 143 ----- .../Effects/OutlineStyleOperation.swift | 2 +- .../RingStyleOperation.swift | 0 Tests/WebUITests/ElementTests.swift | 4 +- examples/InteractionModifiersExample.swift | 285 ---------- 48 files changed, 67 insertions(+), 4328 deletions(-) delete mode 100644 Sources/WebUI/Elements/Base/List.swift delete mode 100644 Sources/WebUI/Elements/Base/Media.swift rename Sources/WebUI/Elements/{Base => Interactive}/Button.swift (100%) rename Sources/WebUI/Elements/{ => Interactive}/Form/Form.swift (100%) rename Sources/WebUI/Elements/{Form => Interactive/Form/Input}/Input.swift (100%) rename Sources/WebUI/Elements/{Form => Interactive/Form/Input}/Label.swift (100%) rename Sources/WebUI/Elements/{ => Interactive}/Form/TextArea.swift (100%) rename Sources/WebUI/Elements/{Form => Interactive}/Progress.swift (100%) delete mode 100644 Sources/WebUI/Elements/Layout/Layout.swift delete mode 100644 Sources/WebUI/Elements/Layout/Structure.swift delete mode 100644 Sources/WebUI/Elements/Progress.swift delete mode 100644 Sources/WebUI/Elements/Structure/Fragment.swift rename Sources/WebUI/Elements/Structure/Layout/{Main.swift => MainElement.swift} (94%) create mode 100644 Sources/WebUI/Elements/Structure/List/List.swift rename Sources/WebUI/Elements/{Base => Text}/Abbreviation.swift (100%) rename Sources/WebUI/Elements/{Base => Text}/Emphasis.swift (100%) rename Sources/WebUI/Elements/{Base => Text}/Link.swift (100%) rename Sources/WebUI/Elements/{Base => Text}/Preformatted.swift (100%) rename Sources/WebUI/Elements/{Base => Text}/Strong.swift (100%) rename Sources/WebUI/Elements/{Base => Text}/Style.swift (100%) rename Sources/WebUI/Elements/{Base => Text}/Text.swift (100%) rename Sources/WebUI/Elements/{Base => Text}/Time.swift (100%) delete mode 100644 Sources/WebUI/Styles/Appearance/BackgroundStyleOperation.swift delete mode 100644 Sources/WebUI/Styles/Appearance/Display/DisplayStyleOperation.swift delete mode 100644 Sources/WebUI/Styles/Appearance/Display/DisplayTypes.swift delete mode 100644 Sources/WebUI/Styles/Appearance/Display/FlexStyleOperation.swift delete mode 100644 Sources/WebUI/Styles/Appearance/Display/GridStyleOperation.swift delete mode 100644 Sources/WebUI/Styles/Appearance/Display/VisibilityStyleOperation.swift delete mode 100644 Sources/WebUI/Styles/Appearance/OpacityStyleOperation.swift delete mode 100644 Sources/WebUI/Styles/Appearance/OutlineStyleOperation.swift delete mode 100644 Sources/WebUI/Styles/Appearance/Shadow/ShadowStyleOperation.swift delete mode 100644 Sources/WebUI/Styles/Appearance/Shadow/ShadowTypes.swift delete mode 100644 Sources/WebUI/Styles/Color/Border/BorderStyleOperation.swift delete mode 100644 Sources/WebUI/Styles/Color/Border/BorderTypes.swift delete mode 100644 Sources/WebUI/Styles/Color/RingStyleOperation.swift delete mode 100644 Sources/WebUI/Styles/Core/InteractionModifiers.swift delete mode 100644 Sources/WebUI/Styles/Core/ResponsiveAlias.swift delete mode 100644 Sources/WebUI/Styles/Core/ResponsiveModifier.swift delete mode 100644 Sources/WebUI/Styles/Core/ResponsiveStyleBuilder.swift delete mode 100644 Sources/WebUI/Styles/Core/ResponsiveStyleModifiers.swift rename Sources/WebUI/Styles/{Appearance => Effects/Border}/BorderRadiusStyleOperation.swift (100%) rename Sources/WebUI/Styles/{Appearance => Effects}/Border/BorderStyleOperation.swift (100%) rename Sources/WebUI/Styles/{Appearance => Effects}/Border/BorderTypes.swift (100%) delete mode 100644 Sources/WebUI/Styles/Effects/BorderRadiusStyleOperation.swift rename Sources/WebUI/Styles/{Appearance => Effects}/RingStyleOperation.swift (100%) delete mode 100644 examples/InteractionModifiersExample.swift 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/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 100% rename from Sources/WebUI/Elements/Form/Input.swift rename to Sources/WebUI/Elements/Interactive/Form/Input/Input.swift 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/Progress.swift b/Sources/WebUI/Elements/Progress.swift deleted file mode 100644 index fb63b65c..00000000 --- a/Sources/WebUI/Elements/Progress.swift +++ /dev/null @@ -1,60 +0,0 @@ -/// Generates an HTML progress element to display task completion status. -/// -/// The progress element visually represents the completion state of a task or process, -/// such as a file download, form submission, or data processing operation. It provides -/// users with visual feedback about ongoing operations. -/// -/// ## Example -/// ```swift -/// Progress(value: 75, max: 100) -/// // Renders: -/// ``` -public final class Progress: Element { - let value: Double? - let max: Double? - - /// Creates a new HTML progress element. - /// - /// - Parameters: - /// - value: Current progress value between 0 and max, optional. When omitted, the progress bar shows an indeterminate state. - /// - max: Maximum progress value (100% completion point), optional. Defaults to 100 when omitted. - /// - id: Unique identifier for the HTML element. - /// - classes: An array of CSS classnames for styling the progress bar. - /// - role: ARIA role of the element for accessibility. - /// - label: ARIA label to describe the element for screen readers (e.g., "Download progress"). - /// - data: Dictionary of `data-*` attributes for storing element-relevant data. - /// - /// ## Example - /// ```swift - /// // Determinate progress bar showing 30% completion - /// Progress(value: 30, max: 100, id: "download-progress", label: "Download progress") - /// - /// // Indeterminate progress bar (activity indicator) - /// Progress(id: "loading-indicator", label: "Loading content") - /// ``` - public init( - value: Double? = nil, - max: Double? = nil, - id: String? = nil, - classes: [String]? = nil, - role: AriaRole? = nil, - label: String? = nil, - data: [String: String]? = nil - ) { - self.value = value - self.max = max - let customAttributes = [ - Attribute.string("value", value?.description), - Attribute.string("max", max?.description), - ].compactMap { $0 } - super.init( - tag: "progress", - id: id, - classes: classes, - role: role, - label: label, - data: data, - customAttributes: customAttributes.isEmpty ? nil : customAttributes - ) - } -} diff --git a/Sources/WebUI/Elements/Structure/Fragment.swift b/Sources/WebUI/Elements/Structure/Fragment.swift deleted file mode 100644 index c1d01bd5..00000000 --- a/Sources/WebUI/Elements/Structure/Fragment.swift +++ /dev/null @@ -1,43 +0,0 @@ -/// Generates a generic HTML fragment without a containing element. -/// -/// Groups arbitrary elements together without rendering a parent tag. -/// Unlike other elements that produce an HTML tag, `Fragment` only renders its children. -/// -/// Use `Fragment` for: -/// - Rendering components that have no obvious root tag -/// - Conditional rendering of multiple elements -/// - Returning multiple elements from a component -/// - Avoiding unnecessary DOM nesting -/// -/// - Note: Conceptually similar to React's Fragment or Swift UI's Group component. -public final class Fragment: HTML { - let contentBuilder: () -> [any HTML]? - - /// Computed inner HTML content. - var content: [any HTML] { - contentBuilder() ?? { [] }() - } - - /// Creates a new HTML fragment that renders only its children. - /// - /// - Parameter content: Closure providing fragment content, defaults to empty. - /// - /// ## Example - /// ```swift - /// Fragment { - /// Heading(.largeTitle) { "Title" } - /// Text { "First paragraph" } - /// Text { "Second paragraph" } - /// } - /// // Renders:

                    Title

                    First paragraph

                    Second paragraph

                    - /// ``` - public init( - @HTMLBuilder content: @escaping () -> [any HTML] = { [] } - ) { - self.contentBuilder = content - } - - public func render() -> String { - content.map { $0.render() }.joined() - } -} diff --git a/Sources/WebUI/Elements/Structure/Layout/Main.swift b/Sources/WebUI/Elements/Structure/Layout/MainElement.swift similarity index 94% rename from Sources/WebUI/Elements/Structure/Layout/Main.swift rename to Sources/WebUI/Elements/Structure/Layout/MainElement.swift index e38a0e98..8bfab59f 100644 --- a/Sources/WebUI/Elements/Structure/Layout/Main.swift +++ b/Sources/WebUI/Elements/Structure/Layout/MainElement.swift @@ -9,13 +9,13 @@ /// ```swift /// Main { /// Heading(.largeTitle) { "Welcome to Our Website" } -/// Text { "This is the main content of our homepage." } +/// Text { "This is the main content of our heomepage." } /// Article { /// // Article content /// } /// } /// ``` -public final class Main: Element { +public final class MainElement: Element { /// Creates a new HTML main element. /// /// - Parameters: diff --git a/Sources/WebUI/Elements/Structure/List/List.swift b/Sources/WebUI/Elements/Structure/List/List.swift new file mode 100644 index 00000000..7629b87d --- /dev/null +++ b/Sources/WebUI/Elements/Structure/List/List.swift @@ -0,0 +1,62 @@ +/// 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 + ) + } +} diff --git a/Sources/WebUI/Elements/Base/Abbreviation.swift b/Sources/WebUI/Elements/Text/Abbreviation.swift similarity index 100% rename from Sources/WebUI/Elements/Base/Abbreviation.swift rename to Sources/WebUI/Elements/Text/Abbreviation.swift diff --git a/Sources/WebUI/Elements/Base/Emphasis.swift b/Sources/WebUI/Elements/Text/Emphasis.swift similarity index 100% rename from Sources/WebUI/Elements/Base/Emphasis.swift rename to Sources/WebUI/Elements/Text/Emphasis.swift diff --git a/Sources/WebUI/Elements/Base/Link.swift b/Sources/WebUI/Elements/Text/Link.swift similarity index 100% rename from Sources/WebUI/Elements/Base/Link.swift rename to Sources/WebUI/Elements/Text/Link.swift diff --git a/Sources/WebUI/Elements/Base/Preformatted.swift b/Sources/WebUI/Elements/Text/Preformatted.swift similarity index 100% rename from Sources/WebUI/Elements/Base/Preformatted.swift rename to Sources/WebUI/Elements/Text/Preformatted.swift diff --git a/Sources/WebUI/Elements/Base/Strong.swift b/Sources/WebUI/Elements/Text/Strong.swift similarity index 100% rename from Sources/WebUI/Elements/Base/Strong.swift rename to Sources/WebUI/Elements/Text/Strong.swift diff --git a/Sources/WebUI/Elements/Base/Style.swift b/Sources/WebUI/Elements/Text/Style.swift similarity index 100% rename from Sources/WebUI/Elements/Base/Style.swift rename to Sources/WebUI/Elements/Text/Style.swift diff --git a/Sources/WebUI/Elements/Base/Text.swift b/Sources/WebUI/Elements/Text/Text.swift similarity index 100% rename from Sources/WebUI/Elements/Base/Text.swift rename to Sources/WebUI/Elements/Text/Text.swift diff --git a/Sources/WebUI/Elements/Base/Time.swift b/Sources/WebUI/Elements/Text/Time.swift similarity index 100% rename from Sources/WebUI/Elements/Base/Time.swift rename to Sources/WebUI/Elements/Text/Time.swift diff --git a/Sources/WebUI/Styles/Appearance/BackgroundStyleOperation.swift b/Sources/WebUI/Styles/Appearance/BackgroundStyleOperation.swift deleted file mode 100644 index d6717822..00000000 --- a/Sources/WebUI/Styles/Appearance/BackgroundStyleOperation.swift +++ /dev/null @@ -1,107 +0,0 @@ -import Foundation - -/// Style operation for background styling -/// -/// Provides a unified implementation for background styling that can be used across -/// Element methods and the Declarative DSL functions. -public struct BackgroundStyleOperation: StyleOperation, @unchecked Sendable { - /// Parameters for background styling - public struct Parameters { - /// The background color - public let color: Color - - /// Creates parameters for background styling - /// - /// - Parameters: - /// - color: The background color - public init(color: Color) { - self.color = color - } - - /// Creates parameters from a StyleParameters container - /// - /// - Parameter params: The style parameters container - /// - Returns: BackgroundStyleOperation.Parameters - public static func from(_ params: StyleParameters) -> Parameters { - Parameters( - color: params.get("color")! - ) - } - } - - /// Applies the background style and returns the appropriate CSS classes - /// - /// - Parameter params: The parameters for background styling - /// - Returns: An array of CSS class names to be applied to elements - public func applyClasses(params: Parameters) -> [String] { - ["bg-\(params.color.rawValue)"] - } - - /// Shared instance for use across the framework - public static let shared = BackgroundStyleOperation() - - /// Private initializer to enforce singleton usage - private init() {} -} - -// Extension for Element to provide background styling -extension Element { - /// Applies background color to the element. - /// - /// Adds a background color class based on the provided color and optional modifiers. - /// This method applies Tailwind CSS background color classes to the element. - /// - /// - Parameters: - /// - color: Sets the background color from the color palette or a custom value. - /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. - /// - Returns: A new element with updated background color classes. - /// - /// ## Example - /// ```swift - /// // Simple background color - /// Button() { "Submit" } - /// .background(color: .green(._500)) - /// - /// // Background color with modifiers - /// Button() { "Hover me" } - /// .background(color: .white, on: .dark) - /// .background(color: .blue(._500), on: .hover) - /// ``` - public func background( - color: Color, - on modifiers: Modifier... - ) -> Element { - let params = BackgroundStyleOperation.Parameters(color: color) - - return BackgroundStyleOperation.shared.applyToElement( - self, - params: params, - modifiers: modifiers - ) - } -} - -// Extension for ResponsiveBuilder to provide background styling -extension ResponsiveBuilder { - /// Applies background color in a responsive context. - /// - /// - Parameter color: The background color. - /// - Returns: The builder for method chaining. - @discardableResult - public func background(color: Color) -> ResponsiveBuilder { - let params = BackgroundStyleOperation.Parameters(color: color) - - return BackgroundStyleOperation.shared.applyToBuilder(self, params: params) - } -} - -// Global function for Declarative DSL -/// Applies background color styling in the responsive context. -/// -/// - Parameter color: The background color. -/// - Returns: A responsive modification for background color. -public func background(color: Color) -> ResponsiveModification { - let params = BackgroundStyleOperation.Parameters(color: color) - - return BackgroundStyleOperation.shared.asModification(params: params) -} diff --git a/Sources/WebUI/Styles/Appearance/Display/DisplayStyleOperation.swift b/Sources/WebUI/Styles/Appearance/Display/DisplayStyleOperation.swift deleted file mode 100644 index 68bb9a32..00000000 --- a/Sources/WebUI/Styles/Appearance/Display/DisplayStyleOperation.swift +++ /dev/null @@ -1,103 +0,0 @@ -import Foundation - -/// Style operation for display styling -/// -/// Provides a unified implementation for display styling that can be used across -/// Element methods and the Declarative DSL functions. -public struct DisplayStyleOperation: StyleOperation, @unchecked Sendable { - /// Parameters for display styling - public struct Parameters { - /// The display type - public let type: DisplayType - - /// Creates parameters for display styling - /// - /// - Parameter type: The display type - public init(type: DisplayType) { - self.type = type - } - - /// Creates parameters from a StyleParameters container - /// - /// - Parameter params: The style parameters container - /// - Returns: DisplayStyleOperation.Parameters - public static func from(_ params: StyleParameters) -> Parameters { - Parameters( - type: params.get("type")! - ) - } - } - - /// Applies the display style and returns the appropriate CSS classes - /// - /// - Parameter params: The parameters for display styling - /// - Returns: An array of CSS class names to be applied to elements - public func applyClasses(params: Parameters) -> [String] { - ["display-\(params.type.rawValue)"] - } - - /// Shared instance for use across the framework - public static let shared = DisplayStyleOperation() - - /// Private initializer to enforce singleton usage - private init() {} -} - -// Extension for Element to provide display styling -extension Element { - /// Sets the CSS display property with optional modifiers. - /// - /// - Parameters: - /// - type: The display type to apply. - /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. - /// - Returns: A new element with updated display classes. - /// - /// ## Example - /// ```swift - /// // Make an element a block - /// Element(tag: "span").display(.block) - /// - /// // Make an element inline-block on hover - /// Element(tag: "div").display(.inlineBlock, on: .hover) - /// - /// // Display as table on medium screens and up - /// Element(tag: "div").display(.table, on: .md) - /// ``` - public func display( - _ type: DisplayType, - on modifiers: Modifier... - ) -> Element { - let params = DisplayStyleOperation.Parameters(type: type) - - return DisplayStyleOperation.shared.applyToElement( - self, - params: params, - modifiers: modifiers - ) - } -} - -// Extension for ResponsiveBuilder to provide display styling -extension ResponsiveBuilder { - /// Sets the CSS display property in a responsive context. - /// - /// - Parameter type: The display type to apply. - /// - Returns: The builder for method chaining. - @discardableResult - public func display(_ type: DisplayType) -> ResponsiveBuilder { - let params = DisplayStyleOperation.Parameters(type: type) - - return DisplayStyleOperation.shared.applyToBuilder(self, params: params) - } -} - -// Global function for Declarative DSL -/// Sets the CSS display property in the responsive context. -/// -/// - Parameter type: The display type to apply. -/// - Returns: A responsive modification for display. -public func display(_ type: DisplayType) -> ResponsiveModification { - let params = DisplayStyleOperation.Parameters(type: type) - - return DisplayStyleOperation.shared.asModification(params: params) -} diff --git a/Sources/WebUI/Styles/Appearance/Display/DisplayTypes.swift b/Sources/WebUI/Styles/Appearance/Display/DisplayTypes.swift deleted file mode 100644 index 21e9d6e1..00000000 --- a/Sources/WebUI/Styles/Appearance/Display/DisplayTypes.swift +++ /dev/null @@ -1,88 +0,0 @@ -/// Defines CSS display types for controlling element rendering. -/// -/// Specifies how an element is displayed in the layout. -public enum DisplayType: String { - /// Makes the element not display at all (removed from layout flow). - case none - /// Standard block element (takes full width, creates new line). - case block - /// Inline element (flows with text, no line breaks). - case inline - /// Hybrid that allows width/height but flows inline. - case inlineBlock = "inline-block" - /// Creates a flex container. - case flex - /// Creates an inline flex container. - case inlineFlex = "inline-flex" - /// Creates a grid container. - case grid - /// Creates an inline grid container. - case inlineGrid = "inline-grid" - /// Creates a table element. - case table - /// Creates a table cell element. - case tableCell = "table-cell" - /// Creates a table row element. - case tableRow = "table-row" -} - -/// Defines justification options for layout alignment. -/// -/// Specifies how items are distributed along the main axis in flexbox or grid layouts. -public enum Justify: String { - /// Aligns items to the start of the horizontal axis. - case start - /// Aligns items to the end of the horizontal axis. - case end - /// Centers items along the horizontal axis. - case center - /// Distributes items with equal space between them. - case between - /// Distributes items with equal space around them. - case around - /// Distributes items with equal space between and around them. - case evenly - - /// Provides the raw CSS class value. - public var rawValue: String { "justify-\(self)" } -} - -/// Represents alignment options for flexbox or grid items. -/// -/// Specifies how items are aligned along the secondary axis in flexbox or grid layouts. -public enum Align: String { - /// Aligns items to the start of the vertical axis - case start - /// Aligns items to the end of the vertical axis - case end - /// Centers items along the vertical axis - case center - /// Aligns items to their baseline - case baseline - /// Stretches items to fill the vertical axis - case stretch - - public var rawValue: String { "items-\(self)" } -} - -/// Represents flexbox direction options. -/// -/// Dictates the direction elements flow in a flexbox layout. -public enum Direction: String { - /// Sets the main axis to horizontal (left to right) - case row = "flex-row" - /// Sets the main axis to vertical (top to bottom) - case column = "flex-col" - /// Sets the main axis to horizontal (right to left) - case rowReverse = "flex-row-reverse" - /// Sets the main axis to vertical (bottom to top) - case colReverse = "flex-col-reverse" -} - -/// Represents a flex grow value; dictates whether the container should fill remaining space -public enum Grow: Int { - /// Indicates the container should not fill remaining space - case zero = 0 - /// Indicates the container should fill remaining space - case one = 1 -} diff --git a/Sources/WebUI/Styles/Appearance/Display/FlexStyleOperation.swift b/Sources/WebUI/Styles/Appearance/Display/FlexStyleOperation.swift deleted file mode 100644 index 0b0119b3..00000000 --- a/Sources/WebUI/Styles/Appearance/Display/FlexStyleOperation.swift +++ /dev/null @@ -1,259 +0,0 @@ -import Foundation - -/// Style operation for flex container styling -/// -/// Provides a unified implementation for flex styling that can be used across -/// Element methods and the Declarative DSL functions. -public struct FlexStyleOperation: StyleOperation, @unchecked Sendable { - /// Parameters for flex styling - public struct Parameters { - /// The flex direction (row, column, etc.) - public let direction: FlexDirection? - - /// The justify content property (start, center, between, etc.) - public let justify: FlexJustify? - - /// The align items property (start, center, end, etc.) - public let align: FlexAlign? - - /// The flex grow property - public let grow: FlexGrow? - - /// Creates parameters for flex styling - /// - /// - Parameters: - /// - direction: The flex direction - /// - justify: The justify content property - /// - align: The align items property - /// - grow: The flex grow property - public init( - direction: FlexDirection? = nil, - justify: FlexJustify? = nil, - align: FlexAlign? = nil, - grow: FlexGrow? = nil - ) { - self.direction = direction - self.justify = justify - self.align = align - self.grow = grow - } - - /// Creates parameters from a StyleParameters container - /// - /// - Parameter params: The style parameters container - /// - Returns: FlexStyleOperation.Parameters - public static func from(_ params: StyleParameters) -> Parameters { - Parameters( - direction: params.get("direction"), - justify: params.get("justify"), - align: params.get("align"), - grow: params.get("grow") - ) - } - } - - /// Applies the flex style and returns the appropriate CSS classes - /// - /// - Parameter params: The parameters for flex styling - /// - Returns: An array of CSS class names to be applied to elements - public func applyClasses(params: Parameters) -> [String] { - var classes = ["flex"] - - if let direction = params.direction { - classes.append("flex-\(direction.rawValue)") - } - - if let justify = params.justify { - classes.append("justify-\(justify.rawValue)") - } - - if let align = params.align { - classes.append("items-\(align.rawValue)") - } - - if let grow = params.grow { - classes.append("flex-\(grow.rawValue)") - } - - return classes - } - - /// Shared instance for use across the framework - public static let shared = FlexStyleOperation() - - /// Private initializer to enforce singleton usage - private init() {} -} - -/// Defines the flex direction -public enum FlexDirection: String { - /// Items are arranged horizontally (left to right) - case row - - /// Items are arranged horizontally in reverse (right to left) - case rowReverse = "row-reverse" - - /// Items are arranged vertically (top to bottom) - case column = "col" - - /// Items are arranged vertically in reverse (bottom to top) - case columnReverse = "col-reverse" -} - -/// Defines how flex items are justified along the main axis -public enum FlexJustify: String { - /// Items are packed at the start of the container - case start - - /// Items are packed at the end of the container - case end - - /// Items are centered along the line - case center - - /// Items are evenly distributed with equal space between them - case between - - /// Items are evenly distributed with equal space around them - case around - - /// Items are evenly distributed with equal space between and around them - case evenly -} - -/// Defines how flex items are aligned along the cross axis -public enum FlexAlign: String { - /// Items are aligned at the start of the cross axis - case start - - /// Items are aligned at the end of the cross axis - case end - - /// Items are centered along the cross axis - case center - - /// Items are stretched to fill the container - case stretch - - /// Items are aligned at the baseline - case baseline -} - -/// Defines the flex grow factor -public enum FlexGrow: String { - /// No growing - case none = "0" - - /// Grow with factor 1 - case one = "1" - - /// Grow with factor 2 - case two = "2" - - /// Grow with factor 3 - case three = "3" - - /// Grow with factor 4 - case four = "4" - - /// Grow with factor 5 - case five = "5" -} - -// Extension for Element to provide flex styling -extension Element { - /// Sets flex container properties with optional modifiers. - /// - /// - Parameters: - /// - direction: The flex direction (row, column, etc.). - /// - justify: How to align items along the main axis. - /// - align: How to align items along the cross axis. - /// - grow: The flex grow factor. - /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. - /// - Returns: A new element with updated flex classes. - /// - /// ## Example - /// ```swift - /// // Create a flex container with column layout - /// Element(tag: "div").flex(direction: .column) - /// - /// // Create a flex container with row layout and centered content - /// Element(tag: "div").flex(direction: .row, justify: .center, align: .center) - /// - /// // Apply flex layout only on medium screens and up - /// Element(tag: "div").flex(direction: .row, on: .md) - /// ``` - public func flex( - direction: FlexDirection? = nil, - justify: FlexJustify? = nil, - align: FlexAlign? = nil, - grow: FlexGrow? = nil, - on modifiers: Modifier... - ) -> Element { - let params = FlexStyleOperation.Parameters( - direction: direction, - justify: justify, - align: align, - grow: grow - ) - - return FlexStyleOperation.shared.applyToElement( - self, - params: params, - modifiers: modifiers - ) - } -} - -// Extension for ResponsiveBuilder to provide flex styling -extension ResponsiveBuilder { - /// Sets flex container properties in a responsive context. - /// - /// - Parameters: - /// - direction: The flex direction (row, column, etc.). - /// - justify: How to align items along the main axis. - /// - align: How to align items along the cross axis. - /// - grow: The flex grow factor. - /// - Returns: The builder for method chaining. - @discardableResult - public func flex( - direction: FlexDirection? = nil, - justify: FlexJustify? = nil, - align: FlexAlign? = nil, - grow: FlexGrow? = nil - ) -> ResponsiveBuilder { - let params = FlexStyleOperation.Parameters( - direction: direction, - justify: justify, - align: align, - grow: grow - ) - - return FlexStyleOperation.shared.applyToBuilder(self, params: params) - } -} - -// Global function for Declarative DSL -/// Sets flex container properties in the responsive context. -/// -/// - Parameters: -/// - direction: The flex direction (row, column, etc.). -/// - justify: How to align items along the main axis. -/// - align: How to align items along the cross axis. -/// - grow: The flex grow factor. -/// - Returns: A responsive modification for flex container. -public func flex( - direction: FlexDirection? = nil, - justify: FlexJustify? = nil, - align: FlexAlign? = nil, - grow: FlexGrow? = nil -) -> ResponsiveModification { - let params = FlexStyleOperation.Parameters( - direction: direction, - justify: justify, - align: align, - grow: grow - ) - - return FlexStyleOperation.shared.asModification(params: params) -} diff --git a/Sources/WebUI/Styles/Appearance/Display/GridStyleOperation.swift b/Sources/WebUI/Styles/Appearance/Display/GridStyleOperation.swift deleted file mode 100644 index 71bb9090..00000000 --- a/Sources/WebUI/Styles/Appearance/Display/GridStyleOperation.swift +++ /dev/null @@ -1,219 +0,0 @@ -import Foundation - -/// Style operation for grid container styling -/// -/// Provides a unified implementation for grid styling that can be used across -/// Element methods and the Declarative DSL functions. -public struct GridStyleOperation: StyleOperation, @unchecked Sendable { - /// Parameters for grid styling - public struct Parameters { - /// The number of grid columns - public let columns: Int? - - /// The number of grid rows - public let rows: Int? - - /// The grid flow direction - public let flow: GridFlow? - - /// The column span value - public let columnSpan: Int? - - /// The row span value - public let rowSpan: Int? - - /// Creates parameters for grid styling - /// - /// - Parameters: - /// - columns: The number of grid columns - /// - rows: The number of grid rows - /// - flow: The grid flow direction - /// - columnSpan: The column span value - /// - rowSpan: The row span value - public init( - columns: Int? = nil, - rows: Int? = nil, - flow: GridFlow? = nil, - columnSpan: Int? = nil, - rowSpan: Int? = nil - ) { - self.columns = columns - self.rows = rows - self.flow = flow - self.columnSpan = columnSpan - self.rowSpan = rowSpan - } - - /// Creates parameters from a StyleParameters container - /// - /// - Parameter params: The style parameters container - /// - Returns: GridStyleOperation.Parameters - public static func from(_ params: StyleParameters) -> Parameters { - Parameters( - columns: params.get("columns"), - rows: params.get("rows"), - flow: params.get("flow"), - columnSpan: params.get("columnSpan"), - rowSpan: params.get("rowSpan") - ) - } - } - - /// Applies the grid style and returns the appropriate CSS classes - /// - /// - Parameter params: The parameters for grid styling - /// - Returns: An array of CSS class names to be applied to elements - public func applyClasses(params: Parameters) -> [String] { - var classes = ["grid"] - - if let columns = params.columns { - classes.append("grid-cols-\(columns)") - } - - if let rows = params.rows { - classes.append("grid-rows-\(rows)") - } - - if let flow = params.flow { - classes.append("grid-flow-\(flow.rawValue)") - } - - if let columnSpan = params.columnSpan { - classes.append("col-span-\(columnSpan)") - } - - if let rowSpan = params.rowSpan { - classes.append("row-span-\(rowSpan)") - } - - return classes - } - - /// Shared instance for use across the framework - public static let shared = GridStyleOperation() - - /// Private initializer to enforce singleton usage - private init() {} -} - -/// Defines the grid flow direction -public enum GridFlow: String { - /// Items flow row by row - case row - - /// Items flow column by column - case col - - /// Items flow row by row, dense packing - case rowDense = "row-dense" - - /// Items flow column by column, dense packing - case colDense = "col-dense" -} - -// Extension for Element to provide grid styling -extension Element { - /// Sets grid container properties with optional modifiers. - /// - /// - Parameters: - /// - columns: The number of grid columns. - /// - rows: The number of grid rows. - /// - flow: The grid flow direction. - /// - columnSpan: The column span value. - /// - rowSpan: The row span value. - /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. - /// - Returns: A new element with updated grid classes. - /// - /// ## Example - /// ```swift - /// // Create a grid container with 3 columns - /// Element(tag: "div").grid(columns: 3) - /// - /// // Create a grid container with 2 columns and 3 rows - /// Element(tag: "div").grid(columns: 2, rows: 3) - /// - /// // Apply grid layout only on medium screens and up - /// Element(tag: "div").grid(columns: 2, on: .md) - /// ``` - public func grid( - columns: Int? = nil, - rows: Int? = nil, - flow: GridFlow? = nil, - columnSpan: Int? = nil, - rowSpan: Int? = nil, - on modifiers: Modifier... - ) -> Element { - let params = GridStyleOperation.Parameters( - columns: columns, - rows: rows, - flow: flow, - columnSpan: columnSpan, - rowSpan: rowSpan - ) - - return GridStyleOperation.shared.applyToElement( - self, - params: params, - modifiers: modifiers - ) - } -} - -// Extension for ResponsiveBuilder to provide grid styling -extension ResponsiveBuilder { - /// Sets grid container properties in a responsive context. - /// - /// - Parameters: - /// - columns: The number of grid columns. - /// - rows: The number of grid rows. - /// - flow: The grid flow direction. - /// - columnSpan: The column span value. - /// - rowSpan: The row span value. - /// - Returns: The builder for method chaining. - @discardableResult - public func grid( - columns: Int? = nil, - rows: Int? = nil, - flow: GridFlow? = nil, - columnSpan: Int? = nil, - rowSpan: Int? = nil - ) -> ResponsiveBuilder { - let params = GridStyleOperation.Parameters( - columns: columns, - rows: rows, - flow: flow, - columnSpan: columnSpan, - rowSpan: rowSpan - ) - - return GridStyleOperation.shared.applyToBuilder(self, params: params) - } -} - -// Global function for Declarative DSL -/// Sets grid container properties in the responsive context. -/// -/// - Parameters: -/// - columns: The number of grid columns. -/// - rows: The number of grid rows. -/// - flow: The grid flow direction. -/// - columnSpan: The column span value. -/// - rowSpan: The row span value. -/// - Returns: A responsive modification for grid container. -public func grid( - columns: Int? = nil, - rows: Int? = nil, - flow: GridFlow? = nil, - columnSpan: Int? = nil, - rowSpan: Int? = nil -) -> ResponsiveModification { - let params = GridStyleOperation.Parameters( - columns: columns, - rows: rows, - flow: flow, - columnSpan: columnSpan, - rowSpan: rowSpan - ) - - return GridStyleOperation.shared.asModification(params: params) -} diff --git a/Sources/WebUI/Styles/Appearance/Display/VisibilityStyleOperation.swift b/Sources/WebUI/Styles/Appearance/Display/VisibilityStyleOperation.swift deleted file mode 100644 index b9b3b08a..00000000 --- a/Sources/WebUI/Styles/Appearance/Display/VisibilityStyleOperation.swift +++ /dev/null @@ -1,110 +0,0 @@ -import Foundation - -/// Style operation for visibility styling -/// -/// Provides a unified implementation for visibility styling that can be used across -/// Element methods and the Declarative DSL functions. -public struct VisibilityStyleOperation: StyleOperation, @unchecked Sendable { - /// Parameters for visibility styling - public struct Parameters { - /// Whether the element should be hidden - public let isHidden: Bool - - /// Creates parameters for visibility styling - /// - /// - Parameter isHidden: Whether the element should be hidden - public init(isHidden: Bool = true) { - self.isHidden = isHidden - } - - /// Creates parameters from a StyleParameters container - /// - /// - Parameter params: The style parameters container - /// - Returns: VisibilityStyleOperation.Parameters - public static func from(_ params: StyleParameters) -> Parameters { - Parameters( - isHidden: params.get("isHidden") ?? true - ) - } - } - - /// Applies the visibility style and returns the appropriate CSS classes - /// - /// - Parameter params: The parameters for visibility styling - /// - Returns: An array of CSS class names to be applied to elements - public func applyClasses(params: Parameters) -> [String] { - if params.isHidden { - return ["hidden"] - } else { - return [] - } - } - - /// Shared instance for use across the framework - public static let shared = VisibilityStyleOperation() - - /// Private initializer to enforce singleton usage - private init() {} -} - -// Extension for Element to provide visibility styling -extension Element { - /// Controls the visibility of an element with optional modifiers. - /// - /// - Parameters: - /// - isHidden: Whether the element should be hidden (default: true). - /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. - /// - Returns: A new element with updated visibility classes. - /// - /// ## Example - /// ```swift - /// // Hide an element - /// Element(tag: "div").hidden() - /// - /// // Show an element on hover - /// Element(tag: "div").hidden(on: .hover) - /// - /// // Hide an element on medium screens and up - /// Element(tag: "div").hidden(on: .md) - /// - /// // Make visible on medium screens - /// Element(tag: "div").hidden(false, on: .md) - /// ``` - public func hidden( - _ isHidden: Bool = true, - on modifiers: Modifier... - ) -> Element { - let params = VisibilityStyleOperation.Parameters(isHidden: isHidden) - - return VisibilityStyleOperation.shared.applyToElement( - self, - params: params, - modifiers: modifiers - ) - } -} - -// Extension for ResponsiveBuilder to provide visibility styling -extension ResponsiveBuilder { - /// Controls the visibility of an element in a responsive context. - /// - /// - Parameter isHidden: Whether the element should be hidden (default: true). - /// - Returns: The builder for method chaining. - @discardableResult - public func hidden(_ isHidden: Bool = true) -> ResponsiveBuilder { - let params = VisibilityStyleOperation.Parameters(isHidden: isHidden) - - return VisibilityStyleOperation.shared.applyToBuilder(self, params: params) - } -} - -// Global function for Declarative DSL -/// Controls the visibility of an element in the responsive context. -/// -/// - Parameter isHidden: Whether the element should be hidden (default: true). -/// - Returns: A responsive modification for visibility. -public func hidden(_ isHidden: Bool = true) -> ResponsiveModification { - let params = VisibilityStyleOperation.Parameters(isHidden: isHidden) - - return VisibilityStyleOperation.shared.asModification(params: params) -} diff --git a/Sources/WebUI/Styles/Appearance/OpacityStyleOperation.swift b/Sources/WebUI/Styles/Appearance/OpacityStyleOperation.swift deleted file mode 100644 index 783325f4..00000000 --- a/Sources/WebUI/Styles/Appearance/OpacityStyleOperation.swift +++ /dev/null @@ -1,98 +0,0 @@ -import Foundation - -/// Style operation for opacity styling -/// -/// Provides a unified implementation for opacity styling that can be used across -/// Element methods and the Declarative DSL functions. -public struct OpacityStyleOperation: StyleOperation, @unchecked Sendable { - /// Parameters for opacity styling - public struct Parameters { - /// The opacity value (0-100) - public let value: Int - - /// Creates parameters for opacity styling - /// - /// - Parameter value: The opacity value (0-100) - public init(value: Int) { - self.value = value - } - - /// Creates parameters from a StyleParameters container - /// - /// - Parameter params: The style parameters container - /// - Returns: OpacityStyleOperation.Parameters - public static func from(_ params: StyleParameters) -> Parameters { - Parameters( - value: params.get("value")! - ) - } - } - - /// Applies the opacity style and returns the appropriate CSS classes - /// - /// - Parameter params: The parameters for opacity styling - /// - Returns: An array of CSS class names to be applied to elements - public func applyClasses(params: Parameters) -> [String] { - ["opacity-\(params.value)"] - } - - /// Shared instance for use across the framework - public static let shared = OpacityStyleOperation() - - /// Private initializer to enforce singleton usage - private init() {} -} - -// Extension for Element to provide opacity styling -extension Element { - /// Sets the opacity of the element with optional modifiers. - /// - /// - Parameters: - /// - value: The opacity value, typically between 0 and 100. - /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. - /// - Returns: A new element with updated opacity classes including applied modifiers. - /// - /// ## Example - /// ```swift - /// Image(source: "/images/profile.jpg", description: "Profile Photo") - /// .opacity(50) - /// .opacity(100, on: .hover) - /// ``` - public func opacity( - _ value: Int, - on modifiers: Modifier... - ) -> Element { - let params = OpacityStyleOperation.Parameters(value: value) - - return OpacityStyleOperation.shared.applyToElement( - self, - params: params, - modifiers: modifiers - ) - } -} - -// Extension for ResponsiveBuilder to provide opacity styling -extension ResponsiveBuilder { - /// Applies opacity styling in a responsive context. - /// - /// - Parameter value: The opacity value (0-100). - /// - Returns: The builder for method chaining. - @discardableResult - public func opacity(_ value: Int) -> ResponsiveBuilder { - let params = OpacityStyleOperation.Parameters(value: value) - - return OpacityStyleOperation.shared.applyToBuilder(self, params: params) - } -} - -// Global function for Declarative DSL -/// Applies opacity styling in the responsive context. -/// -/// - Parameter value: The opacity value (0-100). -/// - Returns: A responsive modification for opacity. -public func opacity(_ value: Int) -> ResponsiveModification { - let params = OpacityStyleOperation.Parameters(value: value) - - return OpacityStyleOperation.shared.asModification(params: params) -} diff --git a/Sources/WebUI/Styles/Appearance/OutlineStyleOperation.swift b/Sources/WebUI/Styles/Appearance/OutlineStyleOperation.swift deleted file mode 100644 index 6539a36b..00000000 --- a/Sources/WebUI/Styles/Appearance/OutlineStyleOperation.swift +++ /dev/null @@ -1,188 +0,0 @@ -import Foundation - -/// Style operation for outline styling -/// -/// Provides a unified implementation for outline styling that can be used across -/// Element methods and the Declarative DSL functions. -public struct OutlineStyleOperation: StyleOperation, @unchecked Sendable { - /// Parameters for outline styling - public struct Parameters { - /// The outline width - public let width: Int? - - /// The outline style (solid, dashed, etc.) - public let style: BorderStyle? - - /// The outline color - public let color: Color? - - /// The outline offset - public let offset: Int? - - /// Creates parameters for outline styling - /// - /// - Parameters: - /// - width: The outline width - /// - style: The outline style - /// - color: The outline color - /// - offset: The outline offset - public init( - width: Int? = nil, - style: BorderStyle? = nil, - color: Color? = nil, - offset: Int? = nil - ) { - self.width = width - self.style = style - self.color = color - self.offset = offset - } - - /// Creates parameters from a StyleParameters container - /// - /// - Parameter params: The style parameters container - /// - Returns: OutlineStyleOperation.Parameters - public static func from(_ params: StyleParameters) -> Parameters { - Parameters( - width: params.get("width"), - style: params.get("style"), - color: params.get("color"), - offset: params.get("offset") - ) - } - } - - /// Applies the outline style and returns the appropriate CSS classes - /// - /// - Parameter params: The parameters for outline styling - /// - Returns: An array of CSS class names to be applied to elements - public func applyClasses(params: Parameters) -> [String] { - var classes = [String]() - - if let width = params.width { - classes.append("outline-\(width)") - } - - if let style = params.style { - classes.append("outline-\(style.rawValue)") - } - - if let color = params.color { - classes.append("outline-\(color.rawValue)") - } - - if let offset = params.offset { - classes.append("outline-offset-\(offset)") - } - - if classes.isEmpty { - classes.append("outline") - } - - return classes - } - - /// Shared instance for use across the framework - public static let shared = OutlineStyleOperation() - - /// Private initializer to enforce singleton usage - private init() {} -} - -// Extension for Element to provide outline styling -extension Element { - /// Sets outline properties with optional modifiers. - /// - /// - Parameters: - /// - width: The outline width. - /// - style: The outline style (solid, dashed, etc.). - /// - color: The outline color. - /// - offset: The outline offset in pixels. - /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. - /// - Returns: A new element with updated outline classes. - /// - /// ## Example - /// ```swift - /// // Add a basic outline - /// Element(tag: "div").outline() - /// - /// // Add a 2px outline with color - /// Element(tag: "div").outline(of: 2, color: .blue(._500)) - /// - /// // Add a dashed outline on focus - /// Element(tag: "div").outline(style: .dashed, on: .focus) - /// ``` - public func outline( - of width: Int? = nil, - style: BorderStyle? = nil, - color: Color? = nil, - offset: Int? = nil, - on modifiers: Modifier... - ) -> Element { - let params = OutlineStyleOperation.Parameters( - width: width, - style: style, - color: color, - offset: offset - ) - - return OutlineStyleOperation.shared.applyToElement( - self, - params: params, - modifiers: modifiers - ) - } -} - -// Extension for ResponsiveBuilder to provide outline styling -extension ResponsiveBuilder { - /// Sets outline properties in a responsive context. - /// - /// - Parameters: - /// - width: The outline width. - /// - style: The outline style (solid, dashed, etc.). - /// - color: The outline color. - /// - offset: The outline offset in pixels. - /// - Returns: The builder for method chaining. - @discardableResult - public func outline( - of width: Int? = nil, - style: BorderStyle? = nil, - color: Color? = nil, - offset: Int? = nil - ) -> ResponsiveBuilder { - let params = OutlineStyleOperation.Parameters( - width: width, - style: style, - color: color, - offset: offset - ) - - return OutlineStyleOperation.shared.applyToBuilder(self, params: params) - } -} - -// Global function for Declarative DSL -/// Sets outline properties in the responsive context. -/// -/// - Parameters: -/// - width: The outline width. -/// - style: The outline style (solid, dashed, etc.). -/// - color: The outline color. -/// - offset: The outline offset in pixels. -/// - Returns: A responsive modification for outline. -public func outline( - of width: Int? = nil, - style: BorderStyle? = nil, - color: Color? = nil, - offset: Int? = nil -) -> ResponsiveModification { - let params = OutlineStyleOperation.Parameters( - width: width, - style: style, - color: color, - offset: offset - ) - - return OutlineStyleOperation.shared.asModification(params: params) -} diff --git a/Sources/WebUI/Styles/Appearance/Shadow/ShadowStyleOperation.swift b/Sources/WebUI/Styles/Appearance/Shadow/ShadowStyleOperation.swift deleted file mode 100644 index 8f69cb35..00000000 --- a/Sources/WebUI/Styles/Appearance/Shadow/ShadowStyleOperation.swift +++ /dev/null @@ -1,155 +0,0 @@ -import Foundation - -/// Style operation for box shadow styling -/// -/// Provides a unified implementation for box shadow styling that can be used across -/// Element methods and the Declaritive DSL functions. -public struct ShadowStyleOperation: StyleOperation, @unchecked Sendable { - /// Parameters for shadow styling - public struct Parameters { - /// The shadow size - public let size: ShadowSize? - - /// The shadow color - public let color: Color? - - /// Creates parameters for shadow styling - /// - /// - Parameters: - /// - size: The shadow size - /// - color: The shadow color - public init( - size: ShadowSize? = nil, - color: Color? = nil - ) { - self.size = size - self.color = color - } - - /// Creates parameters from a StyleParameters container - /// - /// - Parameter params: The style parameters container - /// - Returns: ShadowStyleOperation.Parameters - public static func from(_ params: StyleParameters) -> Parameters { - Parameters( - size: params.get("size"), - color: params.get("color") - ) - } - } - - /// Applies the shadow style and returns the appropriate CSS classes - /// - /// - Parameter params: The parameters for shadow styling - /// - Returns: An array of CSS class names to be applied to elements - public func applyClasses(params: Parameters) -> [String] { - var classes: [String] = [] - let size = params.size ?? ShadowSize.md - let color = params.color - - classes.append("shadow-\(size.rawValue)") - - if let color = color { - classes.append("shadow-\(color.rawValue)") - } - - return classes - } - - /// Shared instance for use across the framework - public static let shared = ShadowStyleOperation() - - /// Private initializer to enforce singleton usage - private init() {} -} - -// Extension for Element to provide shadow styling -extension Element { - /// Applies shadow styling to the element with specified attributes. - /// - /// Adds shadows with custom size and color to an element. - /// - /// - Parameters: - /// - size: The shadow size (sm, md, lg). - /// - color: The shadow color. - /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. - /// - Returns: A new element with updated shadow classes. - /// - /// ## Example - /// ```swift - /// Stack() - /// .shadow(size: .lg, color: .blue(._500)) - /// .shadow(size: .sm, color: .gray(._200), on: .hover) - /// ``` - public func shadow( - size: ShadowSize, - color: Color? = nil, - on modifiers: Modifier... - ) -> Element { - let params = ShadowStyleOperation.Parameters( - size: size, - color: color - ) - - return ShadowStyleOperation.shared.applyToElement( - self, - params: params, - modifiers: modifiers - ) - } -} - -// Extension for ResponsiveBuilder to provide shadow styling -extension ResponsiveBuilder { - /// Applies shadow styling in a responsive context. - /// - /// - Parameters: - /// - size: The shadow size. - /// - color: The shadow color. - /// - Returns: The builder for method chaining. - @discardableResult - public func shadow( - size: ShadowSize? = .md, - color: Color? = nil, - ) -> ResponsiveBuilder { - let params = ShadowStyleOperation.Parameters( - size: size, - color: color - ) - - return ShadowStyleOperation.shared.applyToBuilder(self, params: params) - } - - /// Helper method to apply just a shadow color. - /// - /// - Parameter color: The shadow color to apply. - /// - Returns: The builder for method chaining. - @discardableResult - public func shadow(color: Color) -> ResponsiveBuilder { - let params = ShadowStyleOperation.Parameters( - size: .md, - color: color - ) - - return ShadowStyleOperation.shared.applyToBuilder(self, params: params) - } -} - -// Global function for Declaritive DSL -/// Applies shadow styling in the responsive context. -/// -/// - Parameters: -/// - size: The shadow size. -/// - color: The shadow color. -/// - Returns: A responsive modification for shadows. -public func shadow( - of size: ShadowSize? = .md, - color: Color? = nil -) -> ResponsiveModification { - let params = ShadowStyleOperation.Parameters( - size: size, - color: color - ) - - return ShadowStyleOperation.shared.asModification(params: params) -} diff --git a/Sources/WebUI/Styles/Appearance/Shadow/ShadowTypes.swift b/Sources/WebUI/Styles/Appearance/Shadow/ShadowTypes.swift deleted file mode 100644 index 5f97b5b0..00000000 --- a/Sources/WebUI/Styles/Appearance/Shadow/ShadowTypes.swift +++ /dev/null @@ -1,27 +0,0 @@ -/// Specifies sizes for box shadows. -/// -/// Defines shadow sizes from none to extra-large. -/// -/// ## Example -/// ```swift -/// Stack(classes: ["card"]) -/// .shadow(size: .lg, color: .gray(._300, opacity: 0.5)) -/// ``` -public enum ShadowSize: String { - /// No shadow - case none = "none" - /// Extra small shadow (2xs) - case xs2 = "2xs" - /// Extra small shadow (xs) - case xs = "xs" - /// Small shadow (sm) - case sm = "sm" - /// Medium shadow (default) - case md = "md" - /// Large shadow (lg) - case lg = "lg" - /// Extra large shadow (xl) - case xl = "xl" - /// 2x large shadow (2xl) - case xl2 = "2xl" -} diff --git a/Sources/WebUI/Styles/Color/Border/BorderStyleOperation.swift b/Sources/WebUI/Styles/Color/Border/BorderStyleOperation.swift deleted file mode 100644 index bc2c3970..00000000 --- a/Sources/WebUI/Styles/Color/Border/BorderStyleOperation.swift +++ /dev/null @@ -1,213 +0,0 @@ -import Foundation - -/// Style operation for border styling -/// -/// Provides a unified implementation for border styling that can be used across -/// Element methods and the Declaritive DSL functions. -public struct BorderStyleOperation: StyleOperation, @unchecked Sendable { - /// Parameters for border styling - public struct Parameters { - /// The border width - public let width: Int? - - /// The edges to apply the border to - public let edges: [Edge] - - /// The border style - public let style: BorderStyle? - - /// The border color - public let color: Color? - - /// Creates parameters for border styling - /// - /// - Parameters: - /// - width: The border width - /// - edges: The edges to apply the border to - /// - style: The border style - /// - color: The border color - public init( - width: Int? = 1, - edges: [Edge] = [.all], - style: BorderStyle? = nil, - color: Color? = nil - ) { - self.width = width - self.edges = edges.isEmpty ? [.all] : edges - self.style = style - self.color = color - } - - /// Creates parameters from a StyleParameters container - /// - /// - Parameter params: The style parameters container - /// - Returns: BorderStyleOperation.Parameters - public static func from(_ params: StyleParameters) -> Parameters { - Parameters( - width: params.get("width"), - edges: params.get("edges", default: [.all]), - style: params.get("style"), - color: params.get("color") - ) - } - } - - /// Applies the border style and returns the appropriate CSS classes - /// - /// - Parameter params: The parameters for border styling - /// - Returns: An array of CSS class names to be applied to elements - public func applyClasses(params: Parameters) -> [String] { - var classes: [String] = [] - let width = params.width - let edges = params.edges - let style = params.style - let color = params.color - - for edge in edges { - if let style = style, style == .divide { - if let width = width { - let divideClass = edge == .horizontal ? "divide-x-\(width)" : "divide-y-\(width)" - classes.append(divideClass) - } - } else { - let prefix = edge == .all ? "border" : "border-\(edge.rawValue)" - if let width = width { - classes.append("\(prefix)-\(width)") - } else { - classes.append(prefix) - } - } - } - - if let style = style, style != .divide { - classes.append("border-\(style.rawValue)") - } - - if let color = color { - classes.append("border-\(color.rawValue)") - } - - return classes - } - - /// Shared instance for use across the framework - public static let shared = BorderStyleOperation() - - /// Private initializer to enforce singleton usage - private init() {} -} - -// Extension for Element to provide border styling -extension Element { - /// Applies border styling to the element with specified attributes. - /// - /// Adds borders with custom width, style, and color to specified edges of an element. - /// - /// - Parameters: - /// - width: The border width in pixels. - /// - edges: One or more edges to apply the border to. Defaults to `.all`. - /// - style: The border style (solid, dashed, etc.). - /// - color: The border color. - /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. - /// - Returns: A new element with updated border classes. - /// - /// ## Example - /// ```swift - /// Stack() - /// .border(of: 2, at: .bottom, color: .blue(._500)) - /// .border(of: 1, at: .horizontal, color: .gray(._200), on: .hover) - /// ``` - public func border( - of width: Int? = nil, - at edges: Edge..., - style: BorderStyle? = nil, - color: Color? = nil, - on modifiers: Modifier... - ) -> Element { - let params = BorderStyleOperation.Parameters( - width: width, - edges: edges, - style: style, - color: color - ) - - return BorderStyleOperation.shared.applyToElement( - self, - params: params, - modifiers: modifiers - ) - } -} - -// Extension for ResponsiveBuilder to provide border styling -extension ResponsiveBuilder { - /// Applies border styling in a responsive context. - /// - /// - Parameters: - /// - width: The border width in pixels. - /// - edges: One or more edges to apply the border to. Defaults to `.all`. - /// - style: The border style (solid, dashed, etc.). - /// - color: The border color. - /// - Returns: The builder for method chaining. - @discardableResult - public func border( - of width: Int? = 1, - at edges: Edge..., - style: BorderStyle? = nil, - color: Color? = nil - ) -> ResponsiveBuilder { - let params = BorderStyleOperation.Parameters( - width: width, - edges: edges, - style: style, - color: color - ) - - return BorderStyleOperation.shared.applyToBuilder(self, params: params) - } - - /// Helper method to apply just a border style. - /// - /// - Parameter style: The border style to apply. - /// - Returns: The builder for method chaining. - @discardableResult - public func border(style: BorderStyle) -> ResponsiveBuilder { - let params = BorderStyleOperation.Parameters(style: style) - return BorderStyleOperation.shared.applyToBuilder(self, params: params) - } - - /// Helper method to apply just a border color. - /// - /// - Parameter color: The border color to apply. - /// - Returns: The builder for method chaining. - @discardableResult - public func border(color: Color) -> ResponsiveBuilder { - let params = BorderStyleOperation.Parameters(color: color) - return BorderStyleOperation.shared.applyToBuilder(self, params: params) - } -} - -// Global function for Declaritive DSL -/// Applies border styling in the responsive context. -/// -/// - Parameters: -/// - width: The border width. -/// - edges: The edges to apply the border to. -/// - style: The border style. -/// - color: The border color. -/// - Returns: A responsive modification for borders. -public func border( - of width: Int? = 1, - at edges: Edge..., - style: BorderStyle? = nil, - color: Color? = nil -) -> ResponsiveModification { - let params = BorderStyleOperation.Parameters( - width: width, - edges: edges, - style: style, - color: color - ) - - return BorderStyleOperation.shared.asModification(params: params) -} diff --git a/Sources/WebUI/Styles/Color/Border/BorderTypes.swift b/Sources/WebUI/Styles/Color/Border/BorderTypes.swift deleted file mode 100644 index f6dbc478..00000000 --- a/Sources/WebUI/Styles/Color/Border/BorderTypes.swift +++ /dev/null @@ -1,85 +0,0 @@ -/// Defines sides for applying border radius. -/// -/// Represents individual corners or groups of corners for styling border radius. -/// -/// ## Example -/// ```swift -/// Button() { "Sign Up" } -/// .rounded(.lg, .top) -/// ``` -public enum RadiusSide: String { - /// Applies radius to all corners - case all = "" - /// Applies radius to the top side (top-left and top-right) - case top = "t" - /// Applies radius to the right side (top-right and bottom-right) - case right = "r" - /// Applies radius to the bottom side (bottom-left and bottom-right) - case bottom = "b" - /// Applies radius to the left side (top-left and bottom-left) - case left = "l" - /// Applies radius to the top-left corner - case topLeft = "tl" - /// Applies radius to the top-right corner - case topRight = "tr" - /// Applies radius to the bottom-left corner - case bottomLeft = "bl" - /// Applies radius to the bottom-right corner - case bottomRight = "br" -} - -/// Specifies sizes for border radius. -/// -/// Defines a range of radius values from none to full circular. -/// -/// ## Example -/// ```swift -/// Stack(classes: ["card"]) -/// .rounded(.xl) -/// ``` -public enum RadiusSize: String { - /// No border radius (0) - case none = "none" - /// Extra small radius (0.125rem) - case xs = "xs" - /// Small radius (0.25rem) - case sm = "sm" - /// Medium radius (0.375rem) - case md = "md" - /// Large radius (0.5rem) - case lg = "lg" - /// Extra large radius (0.75rem) - case xl = "xl" - /// 2x large radius (1rem) - case xl2 = "2xl" - /// 3x large radius (1.5rem) - case xl3 = "3xl" - /// Full radius (9999px, circular) - case full = "full" -} - -/// Defines styles for borders and outlines. -/// -/// Provides options for solid, dashed, and other border appearances. -/// -/// ## Example -/// ```swift -/// Stack() -/// .border(width: 1, style: .dashed, color: .gray(._300)) -/// ``` -public enum BorderStyle: String { - /// Solid line border - case solid = "solid" - /// Dashed line border - case dashed = "dashed" - /// Dotted line border - case dotted = "dotted" - /// Double line border - case double = "double" - /// Hidden border (none) - case hidden = "hidden" - /// No border (none) - case none = "none" - /// Divider style for child elements - case divide = "divide" -} diff --git a/Sources/WebUI/Styles/Color/RingStyleOperation.swift b/Sources/WebUI/Styles/Color/RingStyleOperation.swift deleted file mode 100644 index 7fe91b7b..00000000 --- a/Sources/WebUI/Styles/Color/RingStyleOperation.swift +++ /dev/null @@ -1,160 +0,0 @@ -// -// RingStyleOperation.swift -// web-ui -// -// Created by Mac Long on 2025.05.22. -// - -import Foundation - -/// Style operation for ring styling -/// -/// Provides a unified implementation for ring styling that can be used across -/// Element methods and the Declaritive DSL functions. -public struct RingStyleOperation: StyleOperation, @unchecked Sendable { - /// Parameters for ring styling - public struct Parameters { - /// The ring width - public let size: Int? - - /// The ring color - public let color: Color? - - /// Creates parameters for ring styling - /// - /// - Parameters: - /// - size: the width of the ring - /// - color: The ring color - public init( - size: Int? = 1, - color: Color? = nil - ) { - self.size = size - self.color = color - } - - /// Creates parameters from a StyleParameters container - /// - /// - Parameter params: The style parameters container - /// - Returns: RingStyleOperation.Parameters - public static func from(_ params: StyleParameters) -> Parameters { - Parameters( - size: params.get("size"), - color: params.get("color") - ) - } - } - - /// Applies the ring style and returns the appropriate CSS classes - /// - /// - Parameter params: The parameters for ring styling - /// - Returns: An array of CSS class names to be applied to elements - public func applyClasses(params: Parameters) -> [String] { - var classes: [String] = [] - let size = params.size ?? 1 - let color = params.color - - classes.append("ring-\(size)") - - if let color = color { - classes.append("ring-\(color.rawValue)") - } - - return classes - } - - /// Shared instance for use across the framework - public static let shared = RingStyleOperation() - - /// Private initializer to enforce singleton usage - private init() {} -} - -// Extension for Element to provide ring styling -extension Element { - /// Applies ring styling to the element with specified attributes. - /// - /// Adds rings with custom width, style, and color to specified edges of an element. - /// - /// - Parameters: - /// - width: The ring width in pixels. - /// - edges: One or more edges to apply the ring to. Defaults to `.all`. - /// - style: The ring style (solid, dashed, etc.). - /// - color: The ring color. - /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. - /// - Returns: A new element with updated ring classes. - /// - /// ## Example - /// ```swift - /// Stack() - /// .ring(of: 2, at: .bottom, color: .blue(._500)) - /// .ring(of: 1, at: .horizontal, color: .gray(._200), on: .hover) - /// ``` - public func ring( - size: Int = 1, - color: Color? = nil, - on modifiers: Modifier... - ) -> Element { - let params = RingStyleOperation.Parameters( - size: size, - color: color - ) - - return RingStyleOperation.shared.applyToElement( - self, - params: params, - modifiers: modifiers - ) - } -} - -// Extension for ResponsiveBuilder to provide ring styling -extension ResponsiveBuilder { - /// Applies ring styling in a responsive context. - /// - /// - Parameters: - /// - size: The width of the ring. - /// - color: The ring color. - /// - Returns: The builder for method chaining. - @discardableResult - public func ring( - size: Int = 1, - color: Color? = nil - ) -> ResponsiveBuilder { - let params = RingStyleOperation.Parameters( - size: size, - color: color - ) - - return RingStyleOperation.shared.applyToBuilder(self, params: params) - } - - /// Helper method to apply just a ring color. - /// - /// - Parameter color: The ring color to apply. - /// - Returns: The builder for method chaining. - @discardableResult - public func ring(color: Color) -> ResponsiveBuilder { - let params = RingStyleOperation.Parameters(color: color) - return RingStyleOperation.shared.applyToBuilder(self, params: params) - } -} - -// Global function for Declaritive DSL -/// Applies ring styling in the responsive context. -/// -/// - Parameters: -/// - size: The width of the ring. -/// - color: The ring color. -/// - Returns: A responsive modification for rings. -public func ring( - of size: Int = 1, - color: Color? = nil -) -> ResponsiveModification { - let params = RingStyleOperation.Parameters( - size: size, - color: color - ) - - return RingStyleOperation.shared.asModification(params: params) -} diff --git a/Sources/WebUI/Styles/Core/InteractionModifiers.swift b/Sources/WebUI/Styles/Core/InteractionModifiers.swift deleted file mode 100644 index ed268a76..00000000 --- a/Sources/WebUI/Styles/Core/InteractionModifiers.swift +++ /dev/null @@ -1,370 +0,0 @@ -import Foundation - -/// Provides support for interactive states and states modifiers in the WebUI framework. -/// -/// This extension adds support for additional modifiers like hover, focus, and other -/// interactive states to the ResponsiveBuilder to allow styling based on element state. - -extension ResponsiveBuilder { - /// Applies styles when the element is hovered. - /// - /// - Parameter modifications: A closure containing style modifications. - /// - Returns: The builder for method chaining. - @discardableResult - public func hover(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { - currentBreakpoint = .hover - modifications(self) - applyBreakpoint() - return self - } - - /// Applies styles when the element has keyboard focus. - /// - /// - Parameter modifications: A closure containing style modifications. - /// - Returns: The builder for method chaining. - @discardableResult - public func focus(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { - currentBreakpoint = .focus - modifications(self) - applyBreakpoint() - return self - } - - /// Applies styles when the element is being actively pressed or clicked. - /// - /// - Parameter modifications: A closure containing style modifications. - /// - Returns: The builder for method chaining. - @discardableResult - public func active(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { - currentBreakpoint = .active - modifications(self) - applyBreakpoint() - return self - } - - /// Applies styles to input placeholders within the element. - /// - /// - Parameter modifications: A closure containing style modifications. - /// - Returns: The builder for method chaining. - @discardableResult - public func placeholder(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { - currentBreakpoint = .placeholder - modifications(self) - applyBreakpoint() - return self - } - - /// Applies styles when dark mode is active. - /// - /// - Parameter modifications: A closure containing style modifications. - /// - Returns: The builder for method chaining. - @discardableResult - public func dark(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { - currentBreakpoint = .dark - modifications(self) - applyBreakpoint() - return self - } - - /// Applies styles to the first child element. - /// - /// - Parameter modifications: A closure containing style modifications. - /// - Returns: The builder for method chaining. - @discardableResult - public func first(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { - currentBreakpoint = .first - modifications(self) - applyBreakpoint() - return self - } - - /// Applies styles to the last child element. - /// - /// - Parameter modifications: A closure containing style modifications. - /// - Returns: The builder for method chaining. - @discardableResult - public func last(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { - currentBreakpoint = .last - modifications(self) - applyBreakpoint() - return self - } - - /// Applies styles when the element is disabled. - /// - /// - Parameter modifications: A closure containing style modifications. - /// - Returns: The builder for method chaining. - @discardableResult - public func disabled(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { - currentBreakpoint = .disabled - modifications(self) - applyBreakpoint() - return self - } - - /// Applies styles when the user prefers reduced motion. - /// - /// - Parameter modifications: A closure containing style modifications. - /// - Returns: The builder for method chaining. - @discardableResult - public func motionReduce(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { - currentBreakpoint = .motionReduce - modifications(self) - applyBreakpoint() - return self - } - - /// Applies styles when the element has aria-busy="true". - /// - /// - Parameter modifications: A closure containing style modifications. - /// - Returns: The builder for method chaining. - @discardableResult - public func ariaBusy(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { - currentBreakpoint = .ariaBusy - modifications(self) - applyBreakpoint() - return self - } - - /// Applies styles when the element has aria-checked="true". - /// - /// - Parameter modifications: A closure containing style modifications. - /// - Returns: The builder for method chaining. - @discardableResult - public func ariaChecked(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { - currentBreakpoint = .ariaChecked - modifications(self) - applyBreakpoint() - return self - } - - /// Applies styles when the element has aria-disabled="true". - /// - /// - Parameter modifications: A closure containing style modifications. - /// - Returns: The builder for method chaining. - @discardableResult - public func ariaDisabled(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { - currentBreakpoint = .ariaDisabled - modifications(self) - applyBreakpoint() - return self - } - - /// Applies styles when the element has aria-expanded="true". - /// - /// - Parameter modifications: A closure containing style modifications. - /// - Returns: The builder for method chaining. - @discardableResult - public func ariaExpanded(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { - currentBreakpoint = .ariaExpanded - modifications(self) - applyBreakpoint() - return self - } - - /// Applies styles when the element has aria-hidden="true". - /// - /// - Parameter modifications: A closure containing style modifications. - /// - Returns: The builder for method chaining. - @discardableResult - public func ariaHidden(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { - currentBreakpoint = .ariaHidden - modifications(self) - applyBreakpoint() - return self - } - - /// Applies styles when the element has aria-pressed="true". - /// - /// - Parameter modifications: A closure containing style modifications. - /// - Returns: The builder for method chaining. - @discardableResult - public func ariaPressed(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { - currentBreakpoint = .ariaPressed - modifications(self) - applyBreakpoint() - return self - } - - /// Applies styles when the element has aria-readonly="true". - /// - /// - Parameter modifications: A closure containing style modifications. - /// - Returns: The builder for method chaining. - @discardableResult - public func ariaReadonly(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { - currentBreakpoint = .ariaReadonly - modifications(self) - applyBreakpoint() - return self - } - - /// Applies styles when the element has aria-required="true". - /// - /// - Parameter modifications: A closure containing style modifications. - /// - Returns: The builder for method chaining. - @discardableResult - public func ariaRequired(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { - currentBreakpoint = .ariaRequired - modifications(self) - applyBreakpoint() - return self - } - - /// Applies styles when the element has aria-selected="true". - /// - /// - Parameter modifications: A closure containing style modifications. - /// - Returns: The builder for method chaining. - @discardableResult - public func ariaSelected(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { - currentBreakpoint = .ariaSelected - modifications(self) - applyBreakpoint() - return self - } -} - -// MARK: - Responsive DSL Functions - -/// Creates a hover state responsive modification. -/// -/// - Parameter content: A closure containing style modifications for hover state. -/// - Returns: A responsive modification for the hover state. -public func hover(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { - BreakpointModification(breakpoint: .hover, styleModification: content()) -} - -/// Creates a focus state responsive modification. -/// -/// - Parameter content: A closure containing style modifications for focus state. -/// - Returns: A responsive modification for the focus state. -public func focus(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { - BreakpointModification(breakpoint: .focus, styleModification: content()) -} - -/// Creates an active state responsive modification. -/// -/// - Parameter content: A closure containing style modifications for active state. -/// - Returns: A responsive modification for the active state. -public func active(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { - BreakpointModification(breakpoint: .active, styleModification: content()) -} - -/// Creates a placeholder responsive modification. -/// -/// - Parameter content: A closure containing style modifications for placeholder text. -/// - Returns: A responsive modification for placeholder text. -public func placeholder(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { - BreakpointModification(breakpoint: .placeholder, styleModification: content()) -} - -/// Creates a dark mode responsive modification. -/// -/// - Parameter content: A closure containing style modifications for dark mode. -/// - Returns: A responsive modification for dark mode. -public func dark(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { - BreakpointModification(breakpoint: .dark, styleModification: content()) -} - -/// Creates a first-child responsive modification. -/// -/// - Parameter content: A closure containing style modifications for first child elements. -/// - Returns: A responsive modification for first child elements. -public func first(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { - BreakpointModification(breakpoint: .first, styleModification: content()) -} - -/// Creates a last-child responsive modification. -/// -/// - Parameter content: A closure containing style modifications for last child elements. -/// - Returns: A responsive modification for last child elements. -public func last(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { - BreakpointModification(breakpoint: .last, styleModification: content()) -} - -/// Creates a disabled state responsive modification. -/// -/// - Parameter content: A closure containing style modifications for disabled state. -/// - Returns: A responsive modification for the disabled state. -public func disabled(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { - BreakpointModification(breakpoint: .disabled, styleModification: content()) -} - -/// Creates a motion-reduce responsive modification. -/// -/// - Parameter content: A closure containing style modifications for when users prefer reduced motion. -/// - Returns: A responsive modification for reduced motion preferences. -public func motionReduce(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { - BreakpointModification(breakpoint: .motionReduce, styleModification: content()) -} - -/// Creates an aria-busy responsive modification. -/// -/// - Parameter content: A closure containing style modifications for aria-busy="true". -/// - Returns: A responsive modification for the aria-busy state. -public func ariaBusy(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { - BreakpointModification(breakpoint: .ariaBusy, styleModification: content()) -} - -/// Creates an aria-checked responsive modification. -/// -/// - Parameter content: A closure containing style modifications for aria-checked="true". -/// - Returns: A responsive modification for the aria-checked state. -public func ariaChecked(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { - BreakpointModification(breakpoint: .ariaChecked, styleModification: content()) -} - -/// Creates an aria-disabled responsive modification. -/// -/// - Parameter content: A closure containing style modifications for aria-disabled="true". -/// - Returns: A responsive modification for the aria-disabled state. -public func ariaDisabled(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { - BreakpointModification(breakpoint: .ariaDisabled, styleModification: content()) -} - -/// Creates an aria-expanded responsive modification. -/// -/// - Parameter content: A closure containing style modifications for aria-expanded="true". -/// - Returns: A responsive modification for the aria-expanded state. -public func ariaExpanded(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { - BreakpointModification(breakpoint: .ariaExpanded, styleModification: content()) -} - -/// Creates an aria-hidden responsive modification. -/// -/// - Parameter content: A closure containing style modifications for aria-hidden="true". -/// - Returns: A responsive modification for the aria-hidden state. -public func ariaHidden(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { - BreakpointModification(breakpoint: .ariaHidden, styleModification: content()) -} - -/// Creates an aria-pressed responsive modification. -/// -/// - Parameter content: A closure containing style modifications for aria-pressed="true". -/// - Returns: A responsive modification for the aria-pressed state. -public func ariaPressed(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { - BreakpointModification(breakpoint: .ariaPressed, styleModification: content()) -} - -/// Creates an aria-readonly responsive modification. -/// -/// - Parameter content: A closure containing style modifications for aria-readonly="true". -/// - Returns: A responsive modification for the aria-readonly state. -public func ariaReadonly(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { - BreakpointModification(breakpoint: .ariaReadonly, styleModification: content()) -} - -/// Creates an aria-required responsive modification. -/// -/// - Parameter content: A closure containing style modifications for aria-required="true". -/// - Returns: A responsive modification for the aria-required state. -public func ariaRequired(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { - BreakpointModification(breakpoint: .ariaRequired, styleModification: content()) -} - -/// Creates an aria-selected responsive modification. -/// -/// - Parameter content: A closure containing style modifications for aria-selected="true". -/// - Returns: A responsive modification for the aria-selected state. -public func ariaSelected(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { - BreakpointModification(breakpoint: .ariaSelected, styleModification: content()) -} diff --git a/Sources/WebUI/Styles/Core/ResponsiveAlias.swift b/Sources/WebUI/Styles/Core/ResponsiveAlias.swift deleted file mode 100644 index 0ea440fe..00000000 --- a/Sources/WebUI/Styles/Core/ResponsiveAlias.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation - -/// Provides a backward compatibility alias for the new responsive styling API -extension Element { - /// Alias for the `.on` method to maintain backward compatibility with existing code - /// - /// This method provides a backward-compatible way to use the new `.on` method - /// with code that was written to use `.responsive`. - /// - /// - Parameter content: A closure defining responsive style configurations using the result builder. - /// - Returns: An element with responsive styles applied. - public func responsive(@ResponsiveStyleBuilder _ content: () -> ResponsiveModification) -> Element { - on(content) - } -} diff --git a/Sources/WebUI/Styles/Core/ResponsiveModifier.swift b/Sources/WebUI/Styles/Core/ResponsiveModifier.swift deleted file mode 100644 index 014bc1a5..00000000 --- a/Sources/WebUI/Styles/Core/ResponsiveModifier.swift +++ /dev/null @@ -1,225 +0,0 @@ -import Foundation - -/// Provides a block-based responsive design API for WebUI elements. -/// -/// The on modifier allows developers to define responsive styles -/// across different breakpoints in a single, concise block, creating cleaner -/// and more maintainable code for responsive designs. -/// -/// ## Example -/// ```swift -/// Text { "Responsive Content" } -/// .font(size: .sm) -/// .background(color: .neutral(._500)) -/// .on { -/// md { -/// font(size: .lg) -/// background(color: .neutral(._700)) -/// padding(of: 4) -/// } -/// lg { -/// font(size: .xl) -/// background(color: .neutral(._900)) -/// font(alignment: .center) -/// } -/// } -/// ``` -extension Element { - /// Applies responsive styling across different breakpoints with a declarative syntax. - /// - /// This method provides a clean, declarative way to define styles for multiple - /// breakpoints in a single block, improving code readability and maintainability. - /// - /// - Parameter content: A closure defining responsive style configurations using the result builder. - /// - Returns: An element with responsive styles applied. - /// - /// ## Example - /// ```swift - /// Button { "Submit" } - /// .background(color: .blue(._500)) - /// .on { - /// sm { - /// padding(of: 2) - /// font(size: .sm) - /// } - /// md { - /// padding(of: 4) - /// font(size: .base) - /// } - /// lg { - /// padding(of: 6) - /// font(size: .lg) - /// } - /// } - /// ``` - public func on(@ResponsiveStyleBuilder _ content: () -> ResponsiveModification) -> Element { - let builder = ResponsiveBuilder(element: self) - let modification = content() - modification.apply(to: builder) - return builder.element - } -} - -/// Builds responsive style configurations for elements across different breakpoints. -/// -/// `ResponsiveBuilder` provides a fluent, method-chaining API for applying style -/// modifications at specific screen sizes. Each method represents a breakpoint -/// and accepts a closure where style modifications can be defined. -/// -/// This class is not typically created directly, but instead used through the -/// `Element.on(_:)` method. -public class ResponsiveBuilder { - /// The current element being modified - var element: Element - /// Keep track of responsive styles for each breakpoint - internal var pendingClasses: [String] = [] - /// The current breakpoint being modified - internal var currentBreakpoint: Modifier? - - /// Creates a new responsive builder for the given element. - /// - /// - Parameter element: The element to apply responsive styles to. - init(element: Element) { - self.element = element - } - - /// Applies styles at the extra-small breakpoint (480px+). - /// - /// - Parameter modifications: A closure containing style modifications. - /// - Returns: The builder for method chaining. - @discardableResult - public func xs(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { - currentBreakpoint = .xs - modifications(self) - applyBreakpoint() - return self - } - - /// Applies styles at the small breakpoint (640px+). - /// - /// - Parameter modifications: A closure containing style modifications. - /// - Returns: The builder for method chaining. - @discardableResult - public func sm(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { - currentBreakpoint = .sm - modifications(self) - applyBreakpoint() - return self - } - - /// Applies styles at the medium breakpoint (768px+). - /// - /// - Parameter modifications: A closure containing style modifications. - /// - Returns: The builder for method chaining. - @discardableResult - public func md(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { - currentBreakpoint = .md - modifications(self) - applyBreakpoint() - return self - } - - /// Applies styles at the large breakpoint (1024px+). - /// - /// - Parameter modifications: A closure containing style modifications. - /// - Returns: The builder for method chaining. - @discardableResult - public func lg(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { - currentBreakpoint = .lg - modifications(self) - applyBreakpoint() - return self - } - - /// Applies styles at the extra-large breakpoint (1280px+). - /// - /// - Parameter modifications: A closure containing style modifications. - /// - Returns: The builder for method chaining. - @discardableResult - public func xl(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { - currentBreakpoint = .xl - modifications(self) - applyBreakpoint() - return self - } - - /// Applies styles at the 2x-extra-large breakpoint (1536px+). - /// - /// - Parameter modifications: A closure containing style modifications. - /// - Returns: The builder for method chaining. - @discardableResult - public func xl2(_ modifications: (ResponsiveBuilder) -> Void) -> ResponsiveBuilder { - currentBreakpoint = .xl2 - modifications(self) - applyBreakpoint() - return self - } - - /// Applies the breakpoint prefix to all pending classes and add them to the element - internal func applyBreakpoint() { - guard let breakpoint = currentBreakpoint else { return } - - // Apply the breakpoint prefix to all pending classes - let responsiveClasses = pendingClasses.map { - // Handle duplication for flex-*, justify-*, items-* - if $0.starts(with: "flex-") || $0.starts(with: "justify-") || $0.starts(with: "items-") - || $0.starts(with: "grid-") - { - return "\(breakpoint.rawValue)\($0)" - } else if $0 == "flex" || $0 == "grid" { - return "\(breakpoint.rawValue)\($0)" - } else { - return "\(breakpoint.rawValue)\($0)" - } - } - - // Add the responsive classes to the element - self.element = Element( - tag: self.element.tag, - id: self.element.id, - classes: (self.element.classes ?? []) + responsiveClasses, - role: self.element.role, - label: self.element.label, - data: self.element.data, - isSelfClosing: self.element.isSelfClosing, - customAttributes: self.element.customAttributes, - content: self.element.contentBuilder - ) - - // Clear pending classes for the next breakpoint - pendingClasses = [] - currentBreakpoint = nil - } - - /// Add a class to the pending list of classes - public func addClass(_ className: String) { - pendingClasses.append(className) - } -} - -// Font styling methods -extension ResponsiveBuilder { - @discardableResult - public func size(_ value: Int) -> ResponsiveBuilder { - addClass("size-\(value)") - return self - } - - @discardableResult - public func frame( - width: Int? = nil, - height: Int? = nil, - minWidth: Int? = nil, - maxWidth: Int? = nil, - minHeight: Int? = nil, - maxHeight: Int? = nil - ) -> ResponsiveBuilder { - if let width = width { addClass("w-\(width)") } - if let height = height { addClass("h-\(height)") } - if let minWidth = minWidth { addClass("min-w-\(minWidth)") } - if let maxWidth = maxWidth { addClass("max-w-\(maxWidth)") } - if let minHeight = minHeight { addClass("min-h-\(minHeight)") } - if let maxHeight = maxHeight { addClass("max-h-\(maxHeight)") } - return self - } -} diff --git a/Sources/WebUI/Styles/Core/ResponsiveStyleBuilder.swift b/Sources/WebUI/Styles/Core/ResponsiveStyleBuilder.swift deleted file mode 100644 index 9ca7e177..00000000 --- a/Sources/WebUI/Styles/Core/ResponsiveStyleBuilder.swift +++ /dev/null @@ -1,75 +0,0 @@ -import Foundation - -/// A result builder for creating responsive styles with a clean, SwiftUI-like syntax. -/// -/// This builder enables a more natural way to define responsive styles without using `$0` references. -/// -/// ## Example -/// ```swift -/// Element(tag: "div") -/// .responsive { -/// sm { -/// font(size: .base) -/// } -/// md { -/// font(size: .lg) -/// background(color: .blue(._500)) -/// } -/// } -/// ``` -@resultBuilder -public struct ResponsiveStyleBuilder { - /// Builds an empty responsive style. - public static func buildBlock() -> ResponsiveModification { - EmptyResponsiveModification() - } - - /// Builds a responsive style from multiple modifications. - public static func buildBlock(_ components: ResponsiveModification...) -> ResponsiveModification { - CompositeResponsiveModification(modifications: components) - } - - /// Transforms an optional into a responsive modification. - public static func buildOptional(_ component: ResponsiveModification?) -> ResponsiveModification { - component ?? EmptyResponsiveModification() - } - - /// Transforms an either-or condition into a responsive modification. - public static func buildEither(first component: ResponsiveModification) -> ResponsiveModification { - component - } - - /// Transforms an either-or condition into a responsive modification. - public static func buildEither(second component: ResponsiveModification) -> ResponsiveModification { - component - } - - /// Transforms an array of responsive modifications into a single modification. - public static func buildArray(_ components: [ResponsiveModification]) -> ResponsiveModification { - CompositeResponsiveModification(modifications: components) - } -} - -/// Protocol defining the interface for responsive style modifications. -public protocol ResponsiveModification { - /// Applies the modification to the given responsive builder. - func apply(to builder: ResponsiveBuilder) -} - -/// Represents an empty responsive modification. -struct EmptyResponsiveModification: ResponsiveModification { - func apply(to builder: ResponsiveBuilder) { - // Do nothing for empty modifications - } -} - -/// Represents a composite of multiple responsive modifications. -struct CompositeResponsiveModification: ResponsiveModification { - let modifications: [ResponsiveModification] - - func apply(to builder: ResponsiveBuilder) { - for modification in modifications { - modification.apply(to: builder) - } - } -} diff --git a/Sources/WebUI/Styles/Core/ResponsiveStyleModifiers.swift b/Sources/WebUI/Styles/Core/ResponsiveStyleModifiers.swift deleted file mode 100644 index d0d9b45d..00000000 --- a/Sources/WebUI/Styles/Core/ResponsiveStyleModifiers.swift +++ /dev/null @@ -1,214 +0,0 @@ -import Foundation - -/// Provides the implementation for breakpoint and interactive state modifiers in the responsive DSL. -/// -/// These functions are available in the context of a responsive closure, allowing -/// for a more natural, SwiftUI-like syntax without requiring `$0` references. -public struct BreakpointModification: ResponsiveModification { - private let breakpoint: Modifier - private let styleModification: ResponsiveModification - - init(breakpoint: Modifier, styleModification: ResponsiveModification) { - self.breakpoint = breakpoint - self.styleModification = styleModification - } - - public func apply(to builder: ResponsiveBuilder) { - switch breakpoint { - case .xs: - builder.xs { innerBuilder in - styleModification.apply(to: innerBuilder) - } - case .sm: - builder.sm { innerBuilder in - styleModification.apply(to: innerBuilder) - } - case .md: - builder.md { innerBuilder in - styleModification.apply(to: innerBuilder) - } - case .lg: - builder.lg { innerBuilder in - styleModification.apply(to: innerBuilder) - } - case .xl: - builder.xl { innerBuilder in - styleModification.apply(to: innerBuilder) - } - case .xl2: - builder.xl2 { innerBuilder in - styleModification.apply(to: innerBuilder) - } - case .hover: - builder.hover { innerBuilder in - styleModification.apply(to: innerBuilder) - } - case .focus: - builder.focus { innerBuilder in - styleModification.apply(to: innerBuilder) - } - case .active: - builder.active { innerBuilder in - styleModification.apply(to: innerBuilder) - } - case .placeholder: - builder.placeholder { innerBuilder in - styleModification.apply(to: innerBuilder) - } - case .dark: - builder.dark { innerBuilder in - styleModification.apply(to: innerBuilder) - } - case .first: - builder.first { innerBuilder in - styleModification.apply(to: innerBuilder) - } - case .last: - builder.last { innerBuilder in - styleModification.apply(to: innerBuilder) - } - case .disabled: - builder.disabled { innerBuilder in - styleModification.apply(to: innerBuilder) - } - case .motionReduce: - builder.motionReduce { innerBuilder in - styleModification.apply(to: innerBuilder) - } - case .ariaBusy: - builder.ariaBusy { innerBuilder in - styleModification.apply(to: innerBuilder) - } - case .ariaChecked: - builder.ariaChecked { innerBuilder in - styleModification.apply(to: innerBuilder) - } - case .ariaDisabled: - builder.ariaDisabled { innerBuilder in - styleModification.apply(to: innerBuilder) - } - case .ariaExpanded: - builder.ariaExpanded { innerBuilder in - styleModification.apply(to: innerBuilder) - } - case .ariaHidden: - builder.ariaHidden { innerBuilder in - styleModification.apply(to: innerBuilder) - } - case .ariaPressed: - builder.ariaPressed { innerBuilder in - styleModification.apply(to: innerBuilder) - } - case .ariaReadonly: - builder.ariaReadonly { innerBuilder in - styleModification.apply(to: innerBuilder) - } - case .ariaRequired: - builder.ariaRequired { innerBuilder in - styleModification.apply(to: innerBuilder) - } - case .ariaSelected: - builder.ariaSelected { innerBuilder in - styleModification.apply(to: innerBuilder) - } - } - } -} - -/// Represents a style modification in the responsive DSL. -public struct StyleModification: ResponsiveModification { - private let modification: (ResponsiveBuilder) -> Void - - init(_ modification: @escaping (ResponsiveBuilder) -> Void) { - self.modification = modification - } - - public func apply(to builder: ResponsiveBuilder) { - modification(builder) - } -} - -// MARK: - Breakpoint Functions -// Note: Interactive state functions like hover, focus, etc. are defined in InteractionModifiers.swift - -/// Creates an extra-small breakpoint (480px+) responsive modification. -/// -/// - Parameter content: A closure containing style modifications for this breakpoint. -/// - Returns: A responsive modification for the extra-small breakpoint. -public func xs(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { - BreakpointModification(breakpoint: .xs, styleModification: content()) -} - -/// Creates a small breakpoint (640px+) responsive modification. -/// -/// - Parameter content: A closure containing style modifications for this breakpoint. -/// - Returns: A responsive modification for the small breakpoint. -public func sm(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { - BreakpointModification(breakpoint: .sm, styleModification: content()) -} - -/// Creates a medium breakpoint (768px+) responsive modification. -/// -/// - Parameter content: A closure containing style modifications for this breakpoint. -/// - Returns: A responsive modification for the medium breakpoint. -public func md(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { - BreakpointModification(breakpoint: .md, styleModification: content()) -} - -/// Creates a large breakpoint (1024px+) responsive modification. -/// -/// - Parameter content: A closure containing style modifications for this breakpoint. -/// - Returns: A responsive modification for the large breakpoint. -public func lg(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { - BreakpointModification(breakpoint: .lg, styleModification: content()) -} - -/// Creates an extra-large breakpoint (1280px+) responsive modification. -/// -/// - Parameter content: A closure containing style modifications for this breakpoint. -/// - Returns: A responsive modification for the extra-large breakpoint. -public func xl(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { - BreakpointModification(breakpoint: .xl, styleModification: content()) -} - -/// Creates a 2x extra-large breakpoint (1536px+) responsive modification. -/// -/// - Parameter content: A closure containing style modifications for this breakpoint. -/// - Returns: A responsive modification for the 2x extra-large breakpoint. -public func xl2(@ResponsiveStyleBuilder content: () -> ResponsiveModification) -> ResponsiveModification { - BreakpointModification(breakpoint: .xl2, styleModification: content()) -} - -// MARK: - Style Modification Functions - -// Font styling is now implemented in FontStyleOperation.swift - -// Background styling is now implemented in BackgroundStyleOperation.swift - -// Padding styling is now implemented in PaddingStyleOperation.swift - -// Margins styling is now implemented in MarginsStyleOperation.swift - -// Border styling is now implemented in BorderStyleOperation.swift - -// Opacity styling is now implemented in OpacityStyleOperation.swift - -// Size styling is now implemented in SizingStyleOperation.swift - -// Frame styling is now implemented in SizingStyleOperation.swift - -// Flex styling is implemented in ResponsiveBuilder.swift - -// Grid styling is implemented in ResponsiveBuilder.swift - -// Position styling is now implemented in PositionStyleOperation.swift - -// Overflow styling is now implemented in OverflowStyleOperation.swift - -// Hidden styling is implemented in ResponsiveBuilder.swift - -// Border radius styling is now implemented in BorderRadiusStyleOperation.swift - -// Interactive state modifiers like hover, focus, etc. are implemented in InteractionModifiers.swift - -// ARIA state modifiers are implemented in InteractionModifiers.swift diff --git a/Sources/WebUI/Styles/Appearance/BorderRadiusStyleOperation.swift b/Sources/WebUI/Styles/Effects/Border/BorderRadiusStyleOperation.swift similarity index 100% rename from Sources/WebUI/Styles/Appearance/BorderRadiusStyleOperation.swift rename to Sources/WebUI/Styles/Effects/Border/BorderRadiusStyleOperation.swift diff --git a/Sources/WebUI/Styles/Appearance/Border/BorderStyleOperation.swift b/Sources/WebUI/Styles/Effects/Border/BorderStyleOperation.swift similarity index 100% rename from Sources/WebUI/Styles/Appearance/Border/BorderStyleOperation.swift rename to Sources/WebUI/Styles/Effects/Border/BorderStyleOperation.swift diff --git a/Sources/WebUI/Styles/Appearance/Border/BorderTypes.swift b/Sources/WebUI/Styles/Effects/Border/BorderTypes.swift similarity index 100% rename from Sources/WebUI/Styles/Appearance/Border/BorderTypes.swift rename to Sources/WebUI/Styles/Effects/Border/BorderTypes.swift diff --git a/Sources/WebUI/Styles/Effects/BorderRadiusStyleOperation.swift b/Sources/WebUI/Styles/Effects/BorderRadiusStyleOperation.swift deleted file mode 100644 index 86ec1c58..00000000 --- a/Sources/WebUI/Styles/Effects/BorderRadiusStyleOperation.swift +++ /dev/null @@ -1,143 +0,0 @@ -import Foundation - -/// Style operation for border radius styling -/// -/// Provides a unified implementation for border radius styling that can be used across -/// Element methods and the Declarative DSL functions. -public struct BorderRadiusStyleOperation: StyleOperation, @unchecked Sendable { - /// Parameters for border radius styling - public struct Parameters { - /// The border radius size - public let size: RadiusSize? - - /// The sides to apply the radius to - public let sides: [RadiusSide] - - /// Creates parameters for border radius styling - /// - /// - Parameters: - /// - size: The border radius size - /// - sides: The sides to apply the radius to - public init( - size: RadiusSize? = .md, - sides: [RadiusSide] = [.all] - ) { - self.size = size - self.sides = sides.isEmpty ? [.all] : sides - } - - /// Creates parameters from a StyleParameters container - /// - /// - Parameter params: The style parameters container - /// - Returns: BorderRadiusStyleOperation.Parameters - public static func from(_ params: StyleParameters) -> Parameters { - Parameters( - size: params.get("size"), - sides: params.get("sides", default: [.all]) - ) - } - } - - /// Applies the border radius style and returns the appropriate CSS classes - /// - /// - Parameter params: The parameters for border radius styling - /// - Returns: An array of CSS class names to be applied to elements - public func applyClasses(params: Parameters) -> [String] { - var classes: [String] = [] - let size = params.size - let sides = params.sides - - for side in sides { - let sidePrefix = side == .all ? "" : "-\(side.rawValue)" - let sizeValue = size != nil ? "-\(size!.rawValue)" : "" - - classes.append("rounded\(sidePrefix)\(sizeValue)") - } - - return classes - } - - /// Shared instance for use across the framework - public static let shared = BorderRadiusStyleOperation() - - /// Private initializer to enforce singleton usage - private init() {} -} - -// Extension for Element to provide border radius styling -extension Element { - /// Applies border radius styling to the element. - /// - /// - Parameters: - /// - size: The radius size from none to full. - /// - sides: Zero or more sides to apply the radius to. Defaults to all sides. - /// - modifiers: Zero or more modifiers (e.g., `.hover`, `.md`) to scope the styles. - /// - Returns: A new element with updated border radius classes. - /// - /// ## Example - /// ```swift - /// Button() { "Sign Up" } - /// .rounded(.lg) - /// - /// Stack(classes: ["card"]) - /// .rounded(.xl, .top) - /// .rounded(.lg, .bottom, on: .hover) - /// ``` - public func rounded( - _ size: RadiusSize? = .md, - _ sides: RadiusSide..., - on modifiers: Modifier... - ) -> Element { - let params = BorderRadiusStyleOperation.Parameters( - size: size, - sides: sides - ) - - return BorderRadiusStyleOperation.shared.applyToElement( - self, - params: params, - modifiers: modifiers - ) - } -} - -// Extension for ResponsiveBuilder to provide border radius styling -extension ResponsiveBuilder { - /// Applies border radius styling in a responsive context. - /// - /// - Parameters: - /// - size: The radius size from none to full. - /// - sides: Zero or more sides to apply the radius to. - /// - Returns: The builder for method chaining. - @discardableResult - public func rounded( - _ size: RadiusSize? = .md, - _ sides: RadiusSide... - ) -> ResponsiveBuilder { - let params = BorderRadiusStyleOperation.Parameters( - size: size, - sides: sides - ) - - return BorderRadiusStyleOperation.shared.applyToBuilder(self, params: params) - } -} - -// Global function for Declarative DSL -/// Applies border radius styling in the responsive context. -/// -/// - Parameters: -/// - size: The radius size from none to full. -/// - sides: The sides to apply the radius to. -/// - Returns: A responsive modification for border radius. -public func rounded( - _ size: RadiusSize? = .md, - _ sides: RadiusSide... -) -> ResponsiveModification { - let params = BorderRadiusStyleOperation.Parameters( - size: size, - sides: sides - ) - - return BorderRadiusStyleOperation.shared.asModification(params: params) -} diff --git a/Sources/WebUI/Styles/Effects/OutlineStyleOperation.swift b/Sources/WebUI/Styles/Effects/OutlineStyleOperation.swift index 5d279fc7..09f241d4 100644 --- a/Sources/WebUI/Styles/Effects/OutlineStyleOperation.swift +++ b/Sources/WebUI/Styles/Effects/OutlineStyleOperation.swift @@ -185,4 +185,4 @@ public func outline( ) return OutlineStyleOperation.shared.asModification(params: params) -} \ No newline at end of file +} diff --git a/Sources/WebUI/Styles/Appearance/RingStyleOperation.swift b/Sources/WebUI/Styles/Effects/RingStyleOperation.swift similarity index 100% rename from Sources/WebUI/Styles/Appearance/RingStyleOperation.swift rename to Sources/WebUI/Styles/Effects/RingStyleOperation.swift diff --git a/Tests/WebUITests/ElementTests.swift b/Tests/WebUITests/ElementTests.swift index 6b359054..b25c1793 100644 --- a/Tests/WebUITests/ElementTests.swift +++ b/Tests/WebUITests/ElementTests.swift @@ -544,11 +544,11 @@ import Testing @Test("Main element") func testMainElement() async throws { - let main = Main(id: "content") { + let mainElement = Main(id: "content") { Text { "Main content" } } - let rendered = main.render() + let rendered = mainElement.render() #expect(rendered.contains(" String { - Button(disabled: isDisabled, onClick: onClick) { label } - // Base styles - .padding(of: 4) - .rounded(.md) - .transition(of: .all, for: 150) - .font(weight: .medium) - - // Conditional primary/secondary styles - .on { - if isPrimary { - background(color: .blue(._600)) - font(color: .gray(._50)) - } else { - background(color: .gray(._200)) - font(color: .gray(._800)) - border(of: 1, color: .gray(._300)) - } - } - - // Interactive state modifiers - .on { - // Hover state - hover { - if isPrimary { - background(color: .blue(._700)) - } else { - background(color: .gray(._300)) - } - transform(scale: (x: 102, y: 102)) - } - - // Focus state (accessibility) - focus { - if isPrimary { - outline(of: 2, color: .blue(._300)) - } else { - outline(of: 2, color: .gray(._400)) - } - outline(style: .solid) - transform(translateY: -1) - } - - // Active state (when pressing) - active { - if isPrimary { - background(color: .blue(._800)) - } else { - background(color: .gray(._400)) - } - transform(scale: (x: 98, y: 98)) - } - - // Disabled state - disabled { - if isPrimary { - background(color: .blue(._300)) - } else { - background(color: .gray(._100)) - font(color: .gray(._400)) - } - opacity(70) - cursor(.notAllowed) - } - } - .render() - } -} - -/// Form Input Component Example -/// -/// This example demonstrates how to create a form input with various -/// interactive states including placeholder styling and ARIA states. -struct FormInput: HTML { - var id: String - var label: String - var placeholder: String = "" - var isRequired: Bool = false - var isInvalid: Bool = false - var value: String? = nil - - func render() -> String { - Div { - Label(for: id) { label } - .font(size: .sm) - .font(weight: .medium) - .font(color: .gray(._700)) - .on { - if isRequired { - after { - font(color: .red(._500)) - } - } - } - - Input(id: id, value: value, placeholder: placeholder, required: isRequired) - .padding(of: 3) - .rounded(.md) - .border(of: 1, color: .gray(._300)) - .width(.full) - .font(size: .sm) - .transition(of: .all, for: 150) - - // Interactive state styling - .on { - // Placeholder styling - placeholder { - font(color: .gray(._400)) - font(weight: .light) - } - - // Focus state - focus { - border(of: 1, color: .blue(._500)) - shadow(of: .sm, color: .blue(._100)) - } - - // When the field is invalid - if isInvalid { - border(of: 1, color: .red(._500)) - - // Invalid + focus state - focus { - border(of: 1, color: .red(._500)) - shadow(of: .sm, color: .red(._100)) - } - } - - // ARIA required state - ariaRequired { - border(of: 1, style: .solid) - } - } - } - .render() - } -} - -/// Navigation Menu Item Example -/// -/// This example demonstrates how to create an accessible navigation menu item -/// with different states for hover, focus, active, and selected. -struct NavMenuItem: HTML { - var label: String - var href: String - var isSelected: Bool = false - - func render() -> String { - Link(to: href) { label } - .padding(vertical: 2, horizontal: 4) - .rounded(.md) - .font(size: .sm) - .transition(of: .all, for: 150) - - // Base state - .on { - if isSelected { - font(weight: .semibold) - font(color: .blue(._700)) - background(color: .blue(._50)) - } else { - font(weight: .normal) - font(color: .gray(._700)) - } - } - - // Interactive states - .on { - // Hover state - hover { - if !isSelected { - background(color: .gray(._100)) - } else { - background(color: .blue(._100)) - } - } - - // Focus state for keyboard navigation - focus { - outline(of: 2, color: .blue(._300)) - outline(style: .solid) - outline(offset: 1) - } - - // Active state (when pressing) - active { - if !isSelected { - background(color: .gray(._200)) - } else { - background(color: .blue(._200)) - } - transform(scale: (x: 98, y: 98)) - } - - // ARIA selected state for screen readers - ariaSelected { - font(weight: .semibold) - } - } - .render() - } -} - -/// Example usage in a page context -struct InteractiveComponentsDemo: HTML { - func render() -> String { - Document(title: "Interactive Components Demo") { - Section { - Heading(level: 1) { "Interactive Components Demo" } - .font(size: .xl2) - .padding(bottom: 6) - - // Buttons section - Div { - Heading(level: 2) { "Buttons" } - .font(size: .xl) - .padding(bottom: 4) - - Div { - InteractiveButton(label: "Primary Button") - InteractiveButton(label: "Secondary Button", isPrimary: false) - InteractiveButton(label: "Disabled Button", isDisabled: true) - } - .spacing(of: 4) - .display(.flex) - } - .padding(bottom: 8) - - // Form section - Div { - Heading(level: 2) { "Form Inputs" } - .font(size: .xl) - .padding(bottom: 4) - - Div { - FormInput(id: "name", label: "Name", placeholder: "Enter your name") - FormInput(id: "email", label: "Email", placeholder: "Enter your email", isRequired: true) - FormInput( - id: "password", - label: "Password", - placeholder: "Enter your password", - isInvalid: true - ) - } - .spacing(of: 4, along: .vertical) - } - .padding(bottom: 8) - - // Navigation section - Div { - Heading(level: 2) { "Navigation" } - .font(size: .xl) - .padding(bottom: 4) - - Nav { - Div { - NavMenuItem(label: "Home", href: "/", isSelected: true) - NavMenuItem(label: "Products", href: "/products") - NavMenuItem(label: "About", href: "/about") - NavMenuItem(label: "Contact", href: "/contact") - } - .display(.flex) - .spacing(of: 2) - } - } - } - .padding(of: 8) - .maxWidth(.character(80)) - .margins(at: .horizontal, auto: true) - } - .render() - } -} From cb92bfe928d093f0543a103c2665f0e1b721e64c Mon Sep 17 00:00:00 2001 From: Mac Date: Fri, 23 May 2025 13:28:29 +0100 Subject: [PATCH 5/5] Fixed refactor issues with duplicate files --- .../Robots/GenerateRobots.swift | 80 +++++ .../Infrastructure/Robots/RobotsRule.swift | 104 +++++++ .../Sitemap/GenerateSitemap.swift | 117 ++++++++ .../Infrastructure/Sitemap/SitemapEntry.swift | 121 ++++++++ .../WebUI/Elements/Media/Image/Picture.swift | 84 +++--- Sources/WebUI/Elements/Media/MediaSize.swift | 2 +- Sources/WebUI/Elements/Media/Source.swift | 64 ++++ .../Elements/Structure/Layout/Footer.swift | 54 ++++ .../Structure/Layout/MainElement.swift | 2 +- .../Elements/Structure/Layout/Section.swift | 54 ++++ .../WebUI/Elements/Text/Preformatted.swift | 43 ++- Sources/WebUI/Elements/Text/Strong.swift | 31 +- Sources/WebUI/Elements/Text/Text.swift | 283 ------------------ Sources/WebUI/Elements/Text/Time.swift | 37 ++- Sources/WebUI/Styles/Core/Utilities.swift | 4 + .../Effects/OutlineStyleOperation.swift | 34 +-- .../Interactivity/InteractionModifiers.swift | 36 +-- .../Layout/Display/FlexStyleOperation.swift | 68 ++--- .../Layout/Display/GridStyleOperation.swift | 44 +-- .../Display/VisibilityStyleOperation.swift | 18 +- .../Styles/Responsive/ResponsiveAlias.swift | 4 +- 21 files changed, 851 insertions(+), 433 deletions(-) create mode 100644 Sources/WebUI/Core/Infrastructure/Robots/GenerateRobots.swift create mode 100644 Sources/WebUI/Core/Infrastructure/Robots/RobotsRule.swift create mode 100644 Sources/WebUI/Core/Infrastructure/Sitemap/GenerateSitemap.swift create mode 100644 Sources/WebUI/Core/Infrastructure/Sitemap/SitemapEntry.swift create mode 100644 Sources/WebUI/Elements/Media/Source.swift create mode 100644 Sources/WebUI/Elements/Structure/Layout/Footer.swift create mode 100644 Sources/WebUI/Elements/Structure/Layout/Section.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/Elements/Media/Image/Picture.swift b/Sources/WebUI/Elements/Media/Image/Picture.swift index fa3a3438..45ecc250 100644 --- a/Sources/WebUI/Elements/Media/Image/Picture.swift +++ b/Sources/WebUI/Elements/Media/Image/Picture.swift @@ -1,38 +1,51 @@ -import Foundation - -/// Creates HTML picture elements for responsive images. -/// -/// Represents a container for multiple image sources, allowing browsers to choose the most appropriate -/// image format and resolution based on device capabilities. Picture elements enhance website performance -/// by optimizing image delivery based on screen size, resolution, and supported formats. +/// 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: [ -/// (src: "image.webp", type: .webp), -/// (src: "image.jpg", type: .jpeg) +/// ("banner.webp", .webp), +/// ("banner.jpg", .jpeg) /// ], -/// description: "A responsive image", -/// fallback: "fallback.jpg" +/// 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 image MIME type. - /// - description: The alt text for the image for accessibility and SEO. - /// - fallback: Fallback image source URL, 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 picture container. - /// - 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 picture element. + /// - 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)], + sources: [(src: String, type: ImageType?)], description: String, - fallback: String? = nil, size: MediaSize? = nil, id: String? = nil, classes: [String]? = nil, @@ -40,18 +53,9 @@ public final class Picture: Element { label: String? = nil, data: [String: String]? = nil ) { - let sourceElements = sources.map { (src, type) in - "" - }.joined() - let imgTag: String - if let fallback = fallback { - imgTag = "\"\(description)\"" - } else { - imgTag = "\"\(description)\"" - } - let content: () -> [any HTML] = { - [RawHTML(sourceElements + imgTag)] - } + self.sources = sources + self.description = description + self.size = size super.init( tag: "picture", id: id, @@ -59,7 +63,17 @@ public final class Picture: Element { role: role, label: label, data: data, - content: content + 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 index 38ba5f78..4d823049 100644 --- a/Sources/WebUI/Elements/Media/MediaSize.swift +++ b/Sources/WebUI/Elements/Media/MediaSize.swift @@ -16,7 +16,7 @@ public struct MediaSize { public let width: Int? /// The height of the media in pixels. public let height: Int? - + /// Creates a new media size specification. /// /// - Parameters: 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 `