├── Includes └── important.html ├── Assets └── images │ ├── photos │ ├── car.jpg │ ├── rug.jpg │ ├── wind.jpg │ ├── chair.jpg │ ├── shades.jpg │ ├── stack.jpg │ ├── washing.jpg │ └── dishwasher.jpg │ └── logo.svg ├── .swiftlint.yml ├── .gitignore ├── Resources └── quotes.json ├── Content ├── article │ ├── do-not-publish.md │ ├── swift-enterprise-edition.md │ └── swift-against-humanity.md └── story │ ├── sam-swift-saves-the-day.md │ └── luna-and-arya-come-to-wwdc.md ├── Sources ├── .swiftlint.yml ├── Pages │ ├── TagPage.swift │ ├── Examples │ │ ├── QuoteExamples.swift │ │ ├── AlertExamples.swift │ │ ├── EmbedExamples.swift │ │ ├── IncludeExamples.swift │ │ ├── DropdownExamples.swift │ │ ├── BadgeExamples.swift │ │ ├── ImageExamples.swift │ │ ├── LinkExamples.swift │ │ ├── CarouselExamples.swift │ │ ├── CodeExamples.swift │ │ ├── ButtonExamples.swift │ │ ├── ListExamples.swift │ │ ├── AccordionExamples.swift │ │ ├── CardExamples.swift │ │ ├── TableExamples.swift │ │ └── ModalExamples.swift │ ├── Home.swift │ └── Concepts │ │ ├── GridExamples.swift │ │ ├── FormExamples.swift │ │ ├── StylingExamples.swift │ │ ├── ContentExamples.swift │ │ ├── NavigationExamples.swift │ │ ├── TextExamples.swift │ │ └── ThemeExamples.swift ├── Layouts │ ├── MainLayout.swift │ └── SuggestedArticleLayout.swift ├── Components │ ├── VersionInfo.swift │ ├── SocialFooter.swift │ └── NavBar.swift ├── Models │ ├── User.swift │ ├── TeamMember.swift │ └── Customer.swift ├── Configuration │ └── Robots.swift ├── ArticlePages │ ├── CustomStory.swift │ └── Story.swift └── Site.swift ├── README.md ├── Package.swift ├── LICENSE └── CODE_OF_CONDUCT.md /Includes/important.html: -------------------------------------------------------------------------------- 1 |

This is an included file.

-------------------------------------------------------------------------------- /Assets/images/photos/car.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twostraws/IgniteSamples/HEAD/Assets/images/photos/car.jpg -------------------------------------------------------------------------------- /Assets/images/photos/rug.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twostraws/IgniteSamples/HEAD/Assets/images/photos/rug.jpg -------------------------------------------------------------------------------- /Assets/images/photos/wind.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twostraws/IgniteSamples/HEAD/Assets/images/photos/wind.jpg -------------------------------------------------------------------------------- /Assets/images/photos/chair.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twostraws/IgniteSamples/HEAD/Assets/images/photos/chair.jpg -------------------------------------------------------------------------------- /Assets/images/photos/shades.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twostraws/IgniteSamples/HEAD/Assets/images/photos/shades.jpg -------------------------------------------------------------------------------- /Assets/images/photos/stack.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twostraws/IgniteSamples/HEAD/Assets/images/photos/stack.jpg -------------------------------------------------------------------------------- /Assets/images/photos/washing.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twostraws/IgniteSamples/HEAD/Assets/images/photos/washing.jpg -------------------------------------------------------------------------------- /Assets/images/photos/dishwasher.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twostraws/IgniteSamples/HEAD/Assets/images/photos/dishwasher.jpg -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | # Ignore file length, because some pages are huge – example code plus the same as a string really adds up! 2 | disabled_rules: 3 | - file_length 4 | 5 | included: 6 | - Sources 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Build 3 | .build 4 | Packages 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/configuration/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | Package.resolved 11 | -------------------------------------------------------------------------------- /Resources/quotes.json: -------------------------------------------------------------------------------- 1 | [ 2 | "The only limit to our realization of tomorrow will be our doubts of today.", 3 | "It is never too late to be what you might have been.", 4 | "You must be the change you wish to see in the world." 5 | ] -------------------------------------------------------------------------------- /Content/article/do-not-publish.md: -------------------------------------------------------------------------------- 1 | --- 2 | author: Paul Hudson 3 | description: A testing page to check whether the Published property. 4 | date: 2024-05-06 15:30 5 | tags: Testing 6 | published: false 7 | --- 8 | # This article should not be published 9 | 10 | This has the Published property set to false, so this should not be generated. If you see this in the final site, please open an issue! 11 | -------------------------------------------------------------------------------- /Sources/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | # Ignore long lines, because it gets confusing when writing lots of text. 3 | - line_length 4 | 5 | # Ignore long functions, because those are our page bodies. 6 | - function_body_length 7 | 8 | # Ignore using `where` in loops, because it complicates sample code 9 | - for_where 10 | 11 | # Ignore long types; these are example pages deliberately full of examples. 12 | - type_body_length -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IgniteSamples 2 | 3 | This repository contains sample code for the [Ignite static site generator](https://github.com/twostraws/Ignite). 4 | 5 | This repository is covered by the same license, code of conduct, and contributing guidelines as Ignite itself. 6 | 7 | You can see all the output from this repository running here: – it contains lots of sample code right next to previews of how it looks when built. 8 | -------------------------------------------------------------------------------- /Sources/Pages/TagPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TagPage.swift 3 | // IgniteSamples 4 | // https://www.github.com/twostraws/Ignite 5 | // See LICENSE for license information. 6 | // 7 | 8 | import Foundation 9 | import Ignite 10 | 11 | struct Tags: TagPage { 12 | var body: some HTML { 13 | Text(tag.name) 14 | .font(.title1) 15 | 16 | List { 17 | ForEach(tag.articles) { article in 18 | Link(article) 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/Layouts/MainLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainLayout.swift 3 | // IgniteSamples 4 | // https://www.github.com/twostraws/Ignite 5 | // See LICENSE for license information. 6 | // 7 | 8 | import Foundation 9 | import Ignite 10 | 11 | struct MainLayout: Layout { 12 | var body: some Document { 13 | Body { 14 | NavBar() 15 | .padding(.bottom, 100) 16 | 17 | content 18 | 19 | Section { 20 | SocialFooter() 21 | IgniteFooter() 22 | VersionInfo() 23 | .margin(.bottom, 80) 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/Components/VersionInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Version Info.swift 3 | // IgniteSamples 4 | // https://www.github.com/twostraws/Ignite 5 | // See LICENSE for license information. 6 | // 7 | 8 | import Ignite 9 | 10 | /// Displays the version of Ignite the site was built with and the publishing date. 11 | struct VersionInfo: HTML { 12 | private let dateFormat = Date.FormatStyle().month(.defaultDigits).day(.defaultDigits).year(.twoDigits) 13 | var body: some HTML { 14 | Text("\(Ignite.version) · Updated \(Date.now.formatted(dateFormat))") 15 | .horizontalAlignment(.center) 16 | .foregroundStyle(.gray) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/Models/User.swift: -------------------------------------------------------------------------------- 1 | // 2 | // User.swift 3 | // IgniteSamples 4 | // https://www.github.com/twostraws/Ignite 5 | // See LICENSE for license information. 6 | // 7 | import Foundation 8 | 9 | struct User { 10 | var name: String 11 | var address: String 12 | var city: String 13 | var state: String 14 | 15 | static let examples = [ 16 | User(name: "Taylor Swift", address: "555, Swiftie Avenue", city: "Nashville", state: "Tennessee"), 17 | User(name: "Adele Adkins", address: "Caesars Palace", city: "Las Vegas", state: "Nevada"), 18 | User(name: "Tim Cook", address: "Apple Park", city: "Cupertino", state: "California") 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /Sources/Configuration/Robots.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Robots.swift 3 | // IgniteSamples 4 | // https://www.github.com/twostraws/Ignite 5 | // See LICENSE for license information. 6 | // 7 | 8 | import Foundation 9 | import Ignite 10 | 11 | /// An example custom robots.txt configuration file, blocking certain paths 12 | /// from Google and Bing, and everything from ChatGPT. 13 | struct Robots: RobotsConfiguration { 14 | var disallowRules: [DisallowRule] 15 | 16 | init() { 17 | let paths = [ 18 | "/top/secret/stuff", 19 | "/wp-admin" 20 | ] 21 | 22 | disallowRules = [ 23 | DisallowRule(robot: .google, paths: paths), 24 | DisallowRule(robot: .bing, paths: paths), 25 | DisallowRule(robot: .chatGPT) 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/ArticlePages/CustomStory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomStory.swift 3 | // IgniteSamples 4 | // https://www.github.com/twostraws/Ignite 5 | // See LICENSE for license information. 6 | // 7 | 8 | import Foundation 9 | import Ignite 10 | 11 | struct CustomStory: ArticlePage { 12 | var body: some HTML { 13 | if let image = article.image { 14 | Image(image, description: article.imageDescription) 15 | .resizable() 16 | } 17 | 18 | Text(article.title) 19 | .font(.title1) 20 | 21 | if let tagLinks = article.tagLinks() { 22 | HStack(spacing: .xSmall) { 23 | ForEach(tagLinks) { link in 24 | link.font(.title3) 25 | } 26 | } 27 | } 28 | 29 | Text(article.text) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/Models/TeamMember.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TeamMember.swift 3 | // Ignite 4 | // https://www.github.com/twostraws/Ignite 5 | // See LICENSE for license information. 6 | // 7 | 8 | import Foundation 9 | 10 | struct TeamMember { 11 | var name: String 12 | var occupation: String 13 | var favoriteFood: String 14 | var secretHobby: String 15 | 16 | static let examples = [ 17 | TeamMember(name: "Bob Blob", occupation: "Underwater Baker", favoriteFood: "Seaweed Scones", secretHobby: "Collecting Rubber Ducks"), 18 | TeamMember(name: "Sally Sizzle", occupation: "Firefighter Chef", favoriteFood: "Spicy Ice Cream", secretHobby: "Volcano Surfing"), 19 | TeamMember(name: "Greg Grumble", occupation: "Grumpy Florist", favoriteFood: "Thorny Roses Salad", secretHobby: "Whispering to Snails") 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /Sources/ArticlePages/Story.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Story.swift 3 | // IgniteSamples 4 | // https://www.github.com/twostraws/Ignite 5 | // See LICENSE for license information. 6 | // 7 | 8 | import Foundation 9 | import Ignite 10 | 11 | struct Story: ArticlePage { 12 | var body: some HTML { 13 | Text(article.title) 14 | .font(.title1) 15 | 16 | if let image = article.image { 17 | Image(image, description: article.imageDescription) 18 | .resizable() 19 | .cornerRadius(20) 20 | .frame(maxHeight: 300) 21 | } 22 | 23 | if let tags = article.tags { 24 | Section { 25 | Text("Tagged with: \(tags.joined(separator: ", "))") 26 | 27 | Text("\(article.estimatedWordCount) words; \(article.estimatedReadingMinutes) minutes to read.") 28 | } 29 | } 30 | 31 | Text(article.text) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "IgniteSamples", 8 | platforms: [.macOS(.v14)], 9 | dependencies: [ 10 | // The line below makes Ignite Samples use 11 | // the main Ignite repository. 12 | .package(url: "https://github.com/twostraws/Ignite.git", branch: "main") 13 | 14 | // This line makes Ignite Samples use a local 15 | // copy of Ignite, placed one folder up 16 | // from Ignite Samples, which is helpful when 17 | // trying things out locally. 18 | // .package(path: "../Ignite") 19 | ], 20 | targets: [ 21 | // Targets are the basic building blocks of a package, defining a module or a test suite. 22 | // Targets can depend on other targets in this package and products from dependencies. 23 | .executableTarget( 24 | name: "IgniteSamples", 25 | dependencies: ["Ignite"]), 26 | ] 27 | ) 28 | -------------------------------------------------------------------------------- /Sources/Layouts/SuggestedArticleLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SuggestedArticlePage.swift 3 | // IgniteSamples 4 | // https://www.github.com/twostraws/Ignite 5 | // See LICENSE for license information. 6 | // 7 | 8 | import Foundation 9 | import Ignite 10 | 11 | struct SuggestedArticleLayout: Layout { 12 | @Environment(\.articles) private var articles 13 | 14 | var body: some Document { 15 | Body { 16 | NavBar() 17 | 18 | Grid(alignment: .top) { 19 | content 20 | .width(9) 21 | .padding(.vertical, 80) 22 | 23 | Section { 24 | Text("Read this next…") 25 | .font(.title3) 26 | 27 | if let latest = articles.all.randomElement() { 28 | ArticlePreview(for: latest) 29 | } 30 | } 31 | .width(3) 32 | .position(.stickyTop) 33 | .padding(.top, 80) 34 | } 35 | 36 | IgniteFooter() 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/Models/Customer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Customer.swift 3 | // IgniteSamples 4 | // https://www.github.com/twostraws/Ignite 5 | // See LICENSE for license information. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Customer { 11 | var name: String 12 | var city: String 13 | var country: String 14 | 15 | static let examples = [ 16 | Customer(name: "Emily Carter", city: "Toronto", country: "Canada"), 17 | Customer(name: "Lucas Meyer", city: "Berlin", country: "Germany"), 18 | Customer(name: "Sofia Rossi", city: "Rome", country: "Italy"), 19 | Customer(name: "Jack Thompson", city: "Sydney", country: "Australia"), 20 | Customer(name: "Aiko Tanaka", city: "Osaka", country: "Japan"), 21 | Customer(name: "Liam Johnson", city: "Chicago", country: "USA"), 22 | Customer(name: "Chloe Dubois", city: "Lyon", country: "France"), 23 | Customer(name: "Mateo García", city: "Barcelona", country: "Spain"), 24 | Customer(name: "Zanele Mokoena", city: "Johannesburg", country: "South Africa"), 25 | Customer(name: "Arjun Patel", city: "Mumbai", country: "India") 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | IgniteSamples 2 | MIT License 3 | 4 | Copyright (c) 2024 Paul Hudson and other authors. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. -------------------------------------------------------------------------------- /Sources/Components/SocialFooter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavBar.swift 3 | // IgniteSamples 4 | // https://www.github.com/twostraws/Ignite 5 | // See LICENSE for license information. 6 | // 7 | 8 | import Foundation 9 | import Ignite 10 | 11 | /// Displays a global "Social Footer", with each social icon linking to an 12 | /// external site in a new browser tab, demonstrating how to create reusable 13 | /// components with builtIn icons, external links and custom attributes. 14 | struct SocialFooter: HTML { 15 | let icons = [ 16 | Image(systemName: "github"), 17 | Image(systemName: "twitter"), 18 | Image(systemName: "youtube"), 19 | Image(systemName: "mastodon") 20 | ] 21 | 22 | let urlStrings = [ 23 | "https://github.com/twostraws", 24 | "https://twitter.com/twostraws", 25 | "https://youtube.com/@twostraws", 26 | "https://mastodon.social/@twostraws" 27 | ] 28 | 29 | var body: some HTML { 30 | VStack { 31 | HStack { 32 | ForEach(zip(icons, urlStrings)) { (icon, urlString) in 33 | Link(icon, target: urlString) 34 | .role(.secondary) 35 | .target(.blank) 36 | .relationship(.noOpener, .noReferrer) 37 | } 38 | } 39 | } 40 | .margin(.top, .xLarge) 41 | .font(.title2) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/Pages/Examples/QuoteExamples.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuoteExamples.swift 3 | // IgniteSamples 4 | // https://www.github.com/twostraws/Ignite 5 | // See LICENSE for license information. 6 | // 7 | 8 | import Foundation 9 | import Ignite 10 | 11 | struct QuoteExamples: StaticPage { 12 | var title = "Quotes" 13 | var description = """ 14 | A component for highlighting important text or testimonials \ 15 | with optional attribution. Quotes are visually distinct and \ 16 | designed for emphasizing memorable statements. 17 | """ 18 | 19 | var body: some HTML { 20 | Text("Quotes") 21 | .font(.title1) 22 | 23 | Text("Quotes let you mark out important pieces of text, optionally with a source.") 24 | .font(.lead) 25 | 26 | Text("This is a plain quote:") 27 | 28 | CodeBlock(.swift) { 29 | """ 30 | Quote { 31 | Text("It is a truth universally acknowledged that all good Swift projects must be in need of result builders.") 32 | } 33 | """ 34 | } 35 | 36 | Quote { 37 | Text("It is a truth universally acknowledged that all good Swift projects must be in need of result builders.") 38 | } 39 | .margin(.bottom, .xLarge) 40 | 41 | Text("And this is a quote with a caption provided:") 42 | 43 | CodeBlock(.swift) { 44 | """ 45 | Quote { 46 | Text("Programming is an art. Don't spend all your time sharpening your pencil when you should be drawing.") 47 | } caption: { 48 | "Paul Hudson" 49 | } 50 | """ 51 | } 52 | 53 | Quote { 54 | Text("Programming is an art. Don't spend all your time sharpening your pencil when you should be drawing.") 55 | } caption: { 56 | "Paul Hudson" 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/Pages/Examples/AlertExamples.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlertExample.swift 3 | // IgniteSamples 4 | // https://www.github.com/twostraws/Ignite 5 | // See LICENSE for license information. 6 | // 7 | 8 | import Foundation 9 | import Ignite 10 | 11 | struct AlertExamples: StaticPage { 12 | var title = "Alerts" 13 | var description = """ 14 | Display prominent, contextual notifications with semantic styling. \ 15 | Create alerts with different roles for success, warning, error, \ 16 | and information messages, each with distinct visual styling and icons. 17 | """ 18 | 19 | var body: some HTML { 20 | Text("Alerts") 21 | .font(.title1) 22 | 23 | Text("Alerts provide boxed out notifications in a range of styles depending on the role you attach to them.") 24 | .font(.lead) 25 | 26 | Text("A simple alert is created like this:") 27 | 28 | CodeBlock(.swift) { 29 | """ 30 | Alert { 31 | Text("Something went really wrong.") 32 | } 33 | .role(.danger) 34 | """ 35 | } 36 | 37 | Alert { 38 | Text("Something went really wrong.") 39 | } 40 | .role(.danger) 41 | .margin(.bottom, .xLarge) 42 | 43 | Text("Each role applies different styling to the alert, as you can see in this example:") 44 | 45 | CodeBlock(.swift) { 46 | """ 47 | ForEach(Role.semanticRoles) { role in 48 | Alert { 49 | Text("This alert has the \\(role.rawValue) role.") 50 | } 51 | .role(role) 52 | } 53 | """ 54 | } 55 | 56 | ForEach(Role.standardRoles) { role in 57 | Alert { 58 | Text("This alert has the \(role.rawValue) role.") 59 | } 60 | .role(role) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/Site.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Ignite 3 | 4 | @main 5 | struct IgniteWebsite { 6 | static func main() async { 7 | var site = ExampleSite() 8 | 9 | do { 10 | try await site.publish() 11 | } catch { 12 | print(error.localizedDescription) 13 | } 14 | } 15 | } 16 | 17 | struct ExampleSite: Site { 18 | var name = "My Awesome Site" 19 | var titleSuffix = " – My Awesome Site" 20 | var url = URL(static: "https://www.example.com") 21 | 22 | var builtInIconsEnabled = true 23 | var syntaxHighlighterConfiguration: SyntaxHighlighterConfiguration = .init(languages: [.swift, .python, .ruby]) 24 | var feedConfiguration = FeedConfiguration(mode: .full, contentCount: 20, image: .init(url: "https://www.example.com/images/icon32.png", width: 32, height: 32)) 25 | var robotsConfiguration = Robots() 26 | var author = "Paul Hudson" 27 | var useDefaultBootstrapURLs: BootstrapOptions { .remoteBootstrap } 28 | 29 | var homePage = Home() 30 | var tagPage = Tags() 31 | var layout = MainLayout() 32 | 33 | var staticPages: [any StaticPage] { 34 | ContentExamples() 35 | GridExamples() 36 | NavigationExamples() 37 | TextExamples() 38 | FormExamples() 39 | StylingExamples() 40 | ThemeExamples() 41 | 42 | AccordionExamples() 43 | AlertExamples() 44 | BadgeExamples() 45 | ButtonExamples() 46 | CardExamples() 47 | CarouselExamples() 48 | CodeExamples() 49 | DropdownExamples() 50 | EmbedExamples() 51 | ImageExamples() 52 | IncludeExamples() 53 | LinkExamples() 54 | ListExamples() 55 | ModalExamples() 56 | QuoteExamples() 57 | TableExamples() 58 | } 59 | 60 | var articlePages: [any ArticlePage] { 61 | Story() 62 | CustomStory() 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Content/article/swift-enterprise-edition.md: -------------------------------------------------------------------------------- 1 | --- 2 | path: /path/to/enterprise-edition 3 | tags: April Fools 4 | --- 5 | # Apple announces Swift Enterprise Edition 6 | 7 | Although Swift gives us powerful programming technologies such as generics, protocols, and type inference, if there’s one thing we can all agree has been sorely lacking it’s built-in support for scalable concurrent transactions using RESTful middleware and messaging. 8 | 9 | Well, now that’s all set to change: today Apple announced a beta release of Swift Enterprise Edition, which finally delivers support for cutting-edge technologies like CORBA and UML, while also introducing a wholly new way of deploying apps that Apple has christened *Swift Beans*. 10 | 11 | Swift Beans are 100% compatible with Xcode 10.2, meaning that you can immediately start brewing with Swift Enterprise Edition – and, more importantly, add it to your résumé. 12 | 13 | Apple’s official announcement included some quotes from Ed Kermenek, the Swift project lead, giving some further details about how Swift Beans work: “Swift Beans are built on a much more advanced form of Bitcode, called the Apple Portable Runtime Intermediate Language. This lets us layer up abstractions in a way that has never been possible before – factories, builders, transaction services, and more.” 14 | 15 | Several members of Apple’s Swift team have also been working hard on Swift Enterprise Edition, and responded to my questions on Twitter. 16 | 17 | Jay Gorff, lead Swift Bean Brewer, said “Swift Beans are ready for widespread use: this technology give us the grounds to build firm foundations for today, but there’s also a latte you can do with it in the future – it has so many perks!” 18 | 19 | Ben Corne, newly appointed maintainer of the Swift Bean Standard Library, added, “it’s been incredible how fast Swift Beans lets us solve real-world problems. Just yesterday I was able to write an all-new Swift Universal Command-line Kit written entirely in Swift Beans, and if it weren’t for the fact that its name spells ‘SUCK’ we’d be shipping it today.” 20 | 21 | Steve Prestoff, head of public relations for Swift Enterprise Edition, had this to say: “I’m on a horse.” 22 | -------------------------------------------------------------------------------- /Sources/Pages/Home.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Home.swift 3 | // IgniteSamples 4 | // https://www.github.com/twostraws/Ignite 5 | // See LICENSE for license information. 6 | // 7 | 8 | import Foundation 9 | import Ignite 10 | 11 | struct Home: StaticPage { 12 | var title = "Home" 13 | 14 | var body: some HTML { 15 | Text("Welcome to ExampleSite!") 16 | .font(.title1) 17 | 18 | Text("This site is a demonstration of a wide variety of Ignite elements and components all in one place, so you can find code samples for your own sites.") 19 | .font(.lead) 20 | 21 | Text("Key concepts") 22 | .font(.title2) 23 | 24 | Text("Before you create sites yourself, you should review some key concepts that underpin how Ignite works:") 25 | 26 | List { 27 | Link("Grid Layout", target: GridExamples()) 28 | Link("Navigation", target: NavigationExamples()) 29 | Link("Content", target: ContentExamples()) 30 | Link("Text", target: TextExamples()) 31 | Link("Forms", target: FormExamples()) 32 | Link("Styling", target: StylingExamples()) 33 | Link("Theming", target: ThemeExamples()) 34 | } 35 | .listMarkerStyle(.ordered(.automatic)) 36 | 37 | Text("Examples") 38 | .font(.title2) 39 | .margin(.top, .large) 40 | 41 | List { 42 | Link("Accordions", target: AccordionExamples()) 43 | Link("Alerts", target: AlertExamples()) 44 | Link("Badges", target: BadgeExamples()) 45 | Link("Buttons", target: ButtonExamples()) 46 | Link("Cards", target: CardExamples()) 47 | Link("Carousels", target: CarouselExamples()) 48 | Link("Code", target: CodeExamples()) 49 | Link("Dropdowns", target: DropdownExamples()) 50 | Link("Embeds", target: EmbedExamples()) 51 | Link("Images", target: ImageExamples()) 52 | Link("Includes", target: IncludeExamples()) 53 | Link("Links", target: LinkExamples()) 54 | Link("Lists", target: ListExamples()) 55 | Link("Modals", target: ModalExamples()) 56 | Link("Quotes", target: QuoteExamples()) 57 | Link("Tables", target: TableExamples()) 58 | } 59 | .listMarkerStyle(.unordered(.automatic)) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Assets/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 12 | 14 | 16 | 18 | 19 | 21 | 24 | 25 | 26 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /Sources/Pages/Examples/EmbedExamples.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmbedExamples.swift 3 | // IgniteSamples 4 | // https://www.github.com/twostraws/Ignite 5 | // See LICENSE for license information. 6 | // 7 | 8 | import Foundation 9 | import Ignite 10 | 11 | struct EmbedExamples: StaticPage { 12 | var title = "Embeds" 13 | var description = """ 14 | Embed external content like YouTube and Vimeo videos, \ 15 | or entire websites, with proper aspect ratio scaling for responsive layouts. 16 | """ 17 | 18 | var body: some HTML { 19 | Text("Embeds") 20 | .font(.title1) 21 | 22 | Section { 23 | Text("Embeds let you place external content onto your page, such as YouTube or Vimeo videos.") 24 | .font(.lead) 25 | 26 | Alert { 27 | Text(markdown: "**Important:** Always attach an `aspectRatio()` modifier to embeds, so they scale correctly.") 28 | } 29 | .role(.warning) 30 | 31 | Text("For example, here's a YouTube video:") 32 | 33 | CodeBlock(.swift) { 34 | """ 35 | Embed(youTubeID: "dQw4w9WgXcQ", title: "There was only ever going to be one video used here.") 36 | .aspectRatio(.r16x9) 37 | """ 38 | } 39 | 40 | Embed(youTubeID: "dQw4w9WgXcQ", title: "There was only ever going to be one video used here.") 41 | .aspectRatio(.r16x9) 42 | .margin(.bottom, .xLarge) 43 | } 44 | 45 | Section { 46 | Text("Here's a Vimeo video:") 47 | 48 | CodeBlock(.swift) { 49 | """ 50 | Embed(vimeoID: 291590798, title: "Teaching Swift at Scale – Paul Hudson at NSSpain.") 51 | .aspectRatio(.r16x9) 52 | """ 53 | } 54 | 55 | Embed(vimeoID: 291590798, title: "Teaching Swift at Scale – Paul Hudson at NSSpain.") 56 | .aspectRatio(.r16x9) 57 | .margin(.bottom, .xLarge) 58 | 59 | Text("And here's a very important embedded website:") 60 | 61 | CodeBlock(.swift) { 62 | """ 63 | Embed(title: "Upgrade your Mac at Apple.com", url: URL("https://www.zombo.com")) 64 | .aspectRatio(.r16x9) 65 | """ 66 | } 67 | 68 | Embed(title: "Upgrade your Mac at Apple.com", url: URL(string: "https://www.zombo.com")!) 69 | .aspectRatio(.r16x9) 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/Components/NavBar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavBar.swift 3 | // IgniteSamples 4 | // https://www.github.com/twostraws/Ignite 5 | // See LICENSE for license information. 6 | // 7 | 8 | import Foundation 9 | import Ignite 10 | 11 | /// An example navigation bar, demonstrating how to create reusable components. 12 | struct NavBar: HTML { 13 | var body: some HTML { 14 | NavigationBar { 15 | Dropdown { 16 | Link("Grid Layout", target: GridExamples()) 17 | Link("Navigation", target: NavigationExamples()) 18 | Link("Content", target: ContentExamples()) 19 | Link("Text", target: TextExamples()) 20 | Link("Forms", target: FormExamples()) 21 | Link("Styling", target: StylingExamples()) 22 | Link("Theming", target: ThemeExamples()) 23 | } title: { 24 | Span("Key concepts") 25 | .foregroundStyle(.white) 26 | } 27 | 28 | Dropdown { 29 | Link("Accordions", target: AccordionExamples()) 30 | Link("Alerts", target: AlertExamples()) 31 | Link("Badges", target: BadgeExamples()) 32 | Link("Buttons", target: ButtonExamples()) 33 | Link("Cards", target: CardExamples()) 34 | Link("Carousels", target: CarouselExamples()) 35 | Link("Code", target: CodeExamples()) 36 | Link("Dropdowns", target: DropdownExamples()) 37 | Link("Embeds", target: EmbedExamples()) 38 | Link("Images", target: ImageExamples()) 39 | Link("Includes", target: IncludeExamples()) 40 | Link("Links", target: LinkExamples()) 41 | Link("Lists", target: ListExamples()) 42 | Link("Modals", target: ModalExamples()) 43 | Link("Quotes", target: QuoteExamples()) 44 | Link("Tables", target: TableExamples()) 45 | } title: { 46 | Span("Examples") 47 | .foregroundStyle(.white) 48 | } 49 | 50 | Link(target: "https://github.com/twostraws/Ignite") { 51 | Span("Ignite on GitHub") 52 | .foregroundStyle(.white) 53 | } 54 | } logo: { 55 | Image("/images/logo.svg", description: "ExampleSite logo") 56 | .frame(width: .custom("min(60vw, 300px)"), height: .percent(100%)) 57 | } 58 | .navigationItemAlignment(.trailing) 59 | .background(.firebrick) 60 | .position(.fixedTop) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/Pages/Examples/IncludeExamples.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IncludeExamples.swift 3 | // IgniteSamples 4 | // https://www.github.com/twostraws/Ignite 5 | // See LICENSE for license information. 6 | // 7 | 8 | import Foundation 9 | import Ignite 10 | 11 | struct IncludeExamples: StaticPage { 12 | @Environment(\.decode) private var decode 13 | var title = "Includes" 14 | var description = """ 15 | Import HTML content from Includes directory and load external resources \ 16 | from Resources folder, including JSON data decoding. 17 | """ 18 | 19 | var body: some HTML { 20 | Text("Includes") 21 | .font(.title1) 22 | 23 | Text("Includes let you bring in arbitrary HTML from your Includes directory.") 24 | .font(.lead) 25 | 26 | Text(markdown: "For example, the HTML below was imported from **Includes/important.html**") 27 | 28 | CodeBlock(.swift) { 29 | """ 30 | Include("important.html") 31 | """ 32 | } 33 | 34 | Include("important.html") 35 | 36 | Text("Loading arbitrary data") 37 | .font(.title2) 38 | .margin(.top, .xLarge) 39 | 40 | Text(markdown: "If you want to load arbitrary resources for parsing in your Swift code, you should create a folder called **Resources** then you have three options.") 41 | 42 | Text(markdown: "Each option requires you to add `@Environment(\\.decode) var decode` as a property for your page.") 43 | 44 | Text(markdown: "First, `decode.url(forResource:)` tells you the full path to a file in Resources.") 45 | 46 | Text(markdown: "Second, `decode.data(forResource:)` locates a file in Resources and loads it into a `Data` instance. This uses `decode.url(forResource:)` internally.") 47 | 48 | Text(markdown: "And third, `decode(_:as:)` locates a JSON file, loads it into a `Data` instance, then decodes it to a `Decodable` type of your choosing. This uses `decode.data(forResource:)` internally, which in turn uses `decode.url(forResource:)`.") 49 | 50 | Text("For example, this project contains some JSON in Resources/quotes.json, so we can display its contents using the below:") 51 | 52 | CodeBlock(.swift) { 53 | """ 54 | if let quotes = decode("quotes.json", as: [String].self) { 55 | ForEach(quotes) { quote in 56 | Quote { 57 | quote 58 | } 59 | } 60 | } 61 | """ 62 | } 63 | 64 | if let quotes = decode("quotes.json", as: [String].self) { 65 | ForEach(quotes) { quote in 66 | Quote { 67 | quote 68 | } 69 | } 70 | } 71 | 72 | Alert { 73 | Text(markdown: "**Tip:** This page uses a custom theme, which is dark.") 74 | } 75 | .role(.primary) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Sources/Pages/Examples/DropdownExamples.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DropdownExamples.swift 3 | // IgniteSamples 4 | // https://www.github.com/twostraws/Ignite 5 | // See LICENSE for license information. 6 | // 7 | 8 | import Foundation 9 | import Ignite 10 | 11 | struct DropdownExamples: StaticPage { 12 | var title = "Dropdowns" 13 | var description = """ 14 | Create dropdown menus with customizable buttons, links, text, \ 15 | and dividers. Style dropdowns with different roles and sizes, \ 16 | and integrate them seamlessly into navigation \ 17 | bars for enhanced user interaction. 18 | """ 19 | 20 | var body: some HTML { 21 | Text("Dropdowns") 22 | .font(.title1) 23 | 24 | Text("Dropdowns are buttons with links inside, and optionally also text and dividers to break things up.") 25 | .font(.lead) 26 | 27 | CodeBlock(.swift) { 28 | """ 29 | Dropdown("Click Me") { 30 | Link("Accordions", target: AccordionExamples()) 31 | Link("Carousels", target: CarouselExamples()) 32 | Divider() 33 | Text("Or you can just…") 34 | Link("Go back home", target: "/") 35 | } 36 | .role(.primary) 37 | """ 38 | } 39 | 40 | Dropdown("Click Me") { 41 | Link("Accordions", target: AccordionExamples()) 42 | Link("Carousels", target: CarouselExamples()) 43 | Divider() 44 | Text("Or you can just…") 45 | Link("Go back home", target: "/") 46 | } 47 | .role(.primary) 48 | 49 | Text("Styling dropdowns") 50 | .font(.title2) 51 | .margin(.top, .xLarge) 52 | 53 | Text("Like other buttons, dropdowns can have roles and sizes:") 54 | 55 | CodeBlock(.swift) { 56 | """ 57 | ForEach(Role.allCases) { role in 58 | ForEach(Button.Size.allCases) { size in 59 | Dropdown("\\(size.rawValue.capitalized) \\(role.rawValue) dropdown") { 60 | Link("Example Link", target: "#") 61 | } 62 | .dropdownSize(size) 63 | .role(role) 64 | .margin(.bottom, .xSmall) 65 | } 66 | 67 | Spacer(size: .medium) 68 | } 69 | """ 70 | } 71 | 72 | ForEach(Role.allCases) { role in 73 | ForEach(Button.Size.allCases) { size in 74 | Dropdown("\(size.rawValue.capitalized) \(role.rawValue) dropdown") { 75 | Link("Example Link", target: "#") 76 | } 77 | .dropdownSize(size) 78 | .role(role) 79 | .margin(.bottom, .xSmall) 80 | } 81 | 82 | Spacer(size: .medium) 83 | } 84 | 85 | Text(markdown: "Dropdowns in `NavigationBar`") 86 | .font(.title2) 87 | .margin(.top, .xLarge) 88 | 89 | Text(markdown: "Dropdowns also work great in `NavigationBar` elements – there's one up there right now.") 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Content/story/sam-swift-saves-the-day.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: CustomStory 3 | image: /images/photos/wind.jpg 4 | tags: Swift, Story 5 | --- 6 | # Sam Swift saves the day 7 | 8 | Once upon a time in a land not so far away, nestled between the rolling hills of Silicon Valley, there was a developer named **Sam Swift**. Sam, as his name might suggest, was not just any developer; he was a *Swift* developer. A coder of high esteem, known across the land for his quick wit and even quicker compile times. 9 | 10 | One fine day, as Sam was sipping his artisanal coffee and idly scrolling through his endless backlog of emails, he stumbled upon a message that would change the course of his day. The email was from BigTechCorp, a company so large it had its own weather system. They were in dire straits; their flagship app, *Flitter*, was down. Panic had set in, and only Sam's expertise in Swift could save the day. 11 | 12 | With the speed of a thousand asynchronous tasks, Sam leaped into action. He opened his MacBook with such force that the nearby barista spilled a latte in awe. As he began to assess the situation, he couldn't help but chuckle at the absurdity of the bug that had brought **Flitter** to its knees: all because someone had named a variable `let itBe = "Beatles"` instead of `let apiKey = "SECRET"`. 13 | 14 | Determined to not just fix the bug but to do so with style, Sam embarked on a coding spree that would be spoken of in hushed tones for generations to come. He donned his lucky programming socks, cracked his knuckles, and started typing at a speed that blurred the lines between reality and the digital realm. 15 | 16 | First, he refactored the entire backend with a series of extensions that were so elegant, they made the Swift standard library look like a collection of beginner tutorials. He implemented a new feature, `func saveTheDay() -> Bool`, that not only fixed the bug but optimized the app's performance by 300%. The function was a masterpiece, a symphony of code, complete with its own comedic comments: 17 | 18 | ```swift 19 | func saveTheDay() -> Bool { 20 | guard let apiKey = Server.fetch("API_KEY") else { 21 | print("Oops, we did it again. Britney wouldn't be proud.") 22 | return false 23 | } 24 | 25 | // If this code was any cleaner, it'd be in a detergent ad. 26 | performMagicWith(apiKey: apiKey) 27 | return true 28 | } 29 | 30 | // Perform the actual magic, and yes, it's as cool as it sounds. 31 | func performMagicWith(apiKey: String) { 32 | // API calls go here, but they're too magical to show. 33 | } 34 | ``` 35 | 36 | As Sam worked, the world outside seemed to stand still. Tech bloggers live-tweeted his progress, creating a global event that had everyone from New York to New Delhi on the edge of their seats. Memes were made, hashtags were trending, and somewhere in the midst of it all, Sam fixed Flitter. 37 | 38 | With a final keystroke, he pushed the changes live. Instantly, Flitter sprang back to life, much to the relief of BigTechCorp and their billions of users who had been refreshing their feeds in a state of panic. 39 | 40 | And so, our hero lived happily ever after, coding away into the sunset, leaving a trail of bug-free code and laughter in his wake. In the realm of Swift development, Sam Swift became a legend, a beacon of hope for all who face the dreaded specter of app-breaking bugs. His legacy was not just in the code he wrote but in the smiles he brought to faces around the world, proving once and for all that with the right blend of skills, humor, and a bit of Swift, any day could be saved. 41 | 42 | The moral of the story? Always check your variables, keep your functions clean, and remember, in the world of software development, a little humor goes a long way. And if you ever find yourself in a bind, just ask yourself: What Would Sam Swift Do? 43 | -------------------------------------------------------------------------------- /Content/story/luna-and-arya-come-to-wwdc.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: Story 3 | tags: Swift, Story 4 | image: /images/photos/shades.jpg 5 | lastModified: 2024-03-23 6 | --- 7 | # Luna and Arya come to WWDC 8 | 9 | In the heart of Cupertino, amidst the buzz of *WWDC*, a caper of canine proportions was about to unfold, starring two unwitting heroines: *Luna* and *Arya*, the fluffy Samoyeds who are the real power behind Hacking with Swift. Little did anyone know, these two bundles of fur were about to turn Apple Park into their personal playground, all under the watchful eyes of tech enthusiasts and Apple executives alike. 10 | 11 | The adventure began on a sunny Californian morning, as Paul (the dog's owner / helper / treat provider), engrossed in his preparations for a Swift workshop, failed to notice Luna and Arya's growing curiosity about his mysterious destination. Using their sleuth-like senses, the duo decided it was high time they discovered what was so captivating about Paul's "work." 12 | 13 | Through a series of comedic mishaps, including a skateboard chase, a dive into a vat of spilled coffee beans, and an unintentional Lyft ride (courtesy of a kindly driver who couldn't say no to those faces), Luna and Arya found themselves at the gates of Apple Park. With the agility of seasoned escape artists, they slipped through the security perimeter, their fluffy tails just a blur as they dashed into the heart of the action. 14 | 15 | Inside, WWDC was in full swing. Developers from around the globe were immersed in code, oblivious to the furry storm headed their way. Luna and Arya, amazed by the sea of glowing Apple logos, embarked on a whirlwind tour of the premises, leaving a trail of chaos in their wake. From photobombing live interviews to inadvertently launching a demo of the latest visionOS, with a tail wag here and a bark there, they turned the meticulously planned event into an impromptu episode of *America's Funniest Home Videos*. 16 | 17 | As the pandemonium reached its peak, with developers tripping over charging cables and keynote speakers distracted by the sight of two Samoyeds chasing their own shadows on stage, a hero emerged from the shadows. Craig Federighi, Apple's senior vice president of Software Engineering, known for his charismatic stage presence and luxurious mane of hair, stepped forward with a grin. 18 | 19 | Craig, a dog lover himself, knew just what to do. Harnessing the power of Swift, he whipped out his MacBook and began coding on the fly. Within minutes, he had developed an app: "DogWhispererX," using state-of-the-art machine learning algorithms to emit a frequency only Luna and Arya could hear, guiding them gently towards him. 20 | 21 | But Craig didn't stop there. Recognizing a golden opportunity, he turned the situation into the most memorable WWDC keynote ever. With Luna and Arya now calmly by his side, he introduced "Swift Paws," a new framework for developing pet-friendly apps. The crowd went wild, their earlier frustrations forgotten, as they envisioned the possibilities of apps that could strengthen the bond between humans and their furry friends. 22 | 23 | Luna and Arya, now the darlings of WWDC, were escorted back to a relieved and amused owner, who couldn't believe his ears when he heard about their escapades. As a token of gratitude and to much laughter, Craig presented them with custom-made Apple Park Visitor badges, ensuring they'd always be remembered as the Samoyeds who added a touch of whimsy to the world of technology. 24 | 25 | And so, WWDC continued, with developers now sharing not only their love for Swift but also stories of the day when two fluffy intruders reminded them of the joy and unpredictability that pets bring into our lives. Craig Federighi, with his quick thinking and Swift expertise, had saved the day, proving that sometimes, the best solutions come from embracing the unexpected. 26 | 27 | In the end, Paul Hudson's dogs didn't just break into Apple Park; they broke into the hearts of everyone at WWDC, leaving a legacy that would be recounted with smiles and laughter for years to come. And as for Luna and Arya, they returned home, exhausted but exhilarated, dreaming of their next big adventure in the wonderful world of tech. 28 | -------------------------------------------------------------------------------- /Sources/Pages/Examples/BadgeExamples.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BadgeExamples.swift 3 | // IgniteSamples 4 | // https://www.github.com/twostraws/Ignite 5 | // See LICENSE for license information. 6 | // 7 | 8 | import Foundation 9 | import Ignite 10 | 11 | struct BadgeExamples: StaticPage { 12 | var title = "Badges" 13 | var description = """ 14 | Create compact, contextual labels with semantic styling. \ 15 | Design badges with different roles, sizes, and visual \ 16 | styles to highlight counts, tags, and status indicators. \ 17 | Supports default, subtle, and bordered variants for each role. 18 | """ 19 | 20 | var body: some HTML { 21 | Text("Badges") 22 | .font(.title1) 23 | 24 | Text("Badges are small, capsule-shaped elements that are great for showing counts, tags, and other labels.") 25 | .font(.lead) 26 | 27 | Text("A simple badge is created like this:") 28 | 29 | CodeBlock(.swift) { 30 | """ 31 | Text { 32 | Badge("Unread: 500") 33 | .role(.danger) 34 | } 35 | .font(.title2) 36 | """ 37 | } 38 | 39 | Text { 40 | Badge("Unread: 500") 41 | .role(.danger) 42 | } 43 | .font(.title2) 44 | .margin(.bottom, .xLarge) 45 | 46 | Text("Each role applies different styling to the badge, as you can see in this example:") 47 | 48 | CodeBlock(.swift) { 49 | """ 50 | ForEach(Role.standardRoles) { role in 51 | Text { 52 | Badge("This badge has the \\(role.rawValue) role") 53 | .role(role) 54 | } 55 | .font(.lead) 56 | } 57 | """ 58 | } 59 | 60 | ForEach(Role.standardRoles) { role in 61 | Text { 62 | Badge("This badge has the \(role.rawValue) role") 63 | .role(role) 64 | } 65 | .font(.lead) 66 | } 67 | 68 | Text("Badge sizes") 69 | .font(.title2) 70 | .margin(.top, .xLarge) 71 | 72 | Text("Badges automatically adapt to the font of the text element they belong to. For example, here are badges in a range of sizes:") 73 | 74 | CodeBlock(.swift) { 75 | """ 76 | for font in Font.Style.allCases { 77 | Text { 78 | Badge("This badge uses the \\(font) size") 79 | .role(.primary) 80 | } 81 | .font(font) 82 | } 83 | """ 84 | } 85 | 86 | ForEach(Font.Style.allCases) { font in 87 | Text { 88 | Badge("This badge uses the \(font) size") 89 | .role(.primary) 90 | } 91 | .font(font) 92 | } 93 | 94 | Text("Badge variants") 95 | .font(.title2) 96 | .margin(.top, .xLarge) 97 | 98 | Text(markdown: "Each badge role comes in three variants: `.default`, `.subtle`, and `subtleBordered`. These are shown below:") 99 | 100 | CodeBlock(.swift) { 101 | """ 102 | ForEach(Badge.Style.allCases) { style in 103 | Text(markdown: "`\\(style)` style:") 104 | .font(.title3) 105 | 106 | ForEach(Role.standardRoles) { role in 107 | Text { 108 | Badge("This badge has the \\(style) style and \\(role.rawValue) role") 109 | .badgeStyle(style) 110 | .role(role) 111 | } 112 | .font(.lead) 113 | } 114 | 115 | Spacer(size: 40) 116 | } 117 | """ 118 | } 119 | 120 | ForEach(Badge.Style.allCases) { style in 121 | Text(markdown: "`\(style)` style:") 122 | .font(.title3) 123 | .margin(.top, .large) 124 | 125 | ForEach(Role.standardRoles) { role in 126 | Text { 127 | Badge("This badge has the \(style) style and \(role.rawValue) role") 128 | .badgeStyle(style) 129 | .role(role) 130 | } 131 | .font(.lead) 132 | } 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /Sources/Pages/Examples/ImageExamples.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageExamples.swift 3 | // IgniteSamples 4 | // https://www.github.com/twostraws/Ignite 5 | // See LICENSE for license information. 6 | // 7 | 8 | import Foundation 9 | import Ignite 10 | 11 | struct ImageExamples: StaticPage { 12 | var title = "Images" 13 | var description = """ 14 | Display local images and system icons with resizing, \ 15 | lazy loading, and accessibility support. Customize \ 16 | icons with different colors and sizes. 17 | """ 18 | 19 | // This page also demonstrates using a custom layout, as shown below. 20 | var layout = SuggestedArticleLayout() 21 | 22 | var body: some HTML { 23 | Text("Images") 24 | .font(.title1) 25 | 26 | Section { 27 | Text(markdown: "Images can be either pictures from your **/Assets/images** folder, or one of the built-in icons.") 28 | .font(.lead) 29 | 30 | Text("Pictures are shown at their natural size by default; you'll usually want to add the `resizable()` modifier to make them scalable:") 31 | 32 | CodeBlock(.swift) { 33 | """ 34 | Image("/images/photos/rug.jpg", description: "A rug. Not a dog, a rug.") 35 | .resizable() 36 | """ 37 | } 38 | 39 | Image("/images/photos/rug.jpg", description: "A rug. Not a dog, a rug.") 40 | .resizable() 41 | .margin(.bottom, .large) 42 | 43 | Alert { 44 | Text(markdown: "Most images should be created with a `description` value provided, for screen readers.") 45 | } 46 | .role(.info) 47 | 48 | Text(markdown: "If you're working with large images, consider using the `lazy()` modifier to have them loaded lazily.") 49 | 50 | Text("Icons") 51 | .font(.title2) 52 | .margin(.top, .xLarge) 53 | 54 | Text(markdown: "You can also create images from icons. If they are inside `Text`, they will resize with the font:") 55 | 56 | CodeBlock(.swift) { 57 | """ 58 | let icons = ["airplane", "apple", "arrow-counterclockwise", "award", "balloon", "book", "brightness-high"] 59 | 60 | ForEach(Font.Style.allCases) { font in 61 | Text { 62 | for icon in icons { 63 | Image(systemName: icon) 64 | .margin(.trailing, 20) 65 | } 66 | } 67 | .font(font) 68 | } 69 | """ 70 | } 71 | } 72 | 73 | let icons = ["airplane", "apple", "arrow-counterclockwise", "award", "balloon", "book", "brightness-high"] 74 | 75 | ForEach(Font.Style.allCases) { font in 76 | Text { 77 | InlineForEach(icons) { icon in 78 | Image(systemName: icon) 79 | .margin(.trailing, 20) 80 | } 81 | } 82 | .font(font) 83 | } 84 | 85 | Text(markdown: "Use the `foregroundStyle()` modifier to recolor your icons, like this:") 86 | .margin(.top, .xLarge) 87 | 88 | CodeBlock(.swift) { 89 | """ 90 | let colors = [Color.green, .blue, .indigo, .slateGray, .gold, .orange, .tomato, .gray] 91 | 92 | Text { 93 | ForEach(zip(icons, colors)) { icon, color in 94 | Image(systemName: icon) 95 | .foregroundStyle(color) 96 | .margin(.trailing, 20) 97 | } 98 | } 99 | .font(.title1) 100 | """ 101 | } 102 | 103 | let colors = [Color.green, .blue, .indigo, .slateGray, .gold, .orange, .tomato, .gray] 104 | 105 | Text { 106 | InlineForEach(zip(icons, colors)) { icon, color in 107 | Image(systemName: icon) 108 | .foregroundStyle(color) 109 | .margin(.trailing, 20) 110 | } 111 | } 112 | .font(.title1) 113 | .margin(.bottom, .large) 114 | 115 | Text(markdown: "**Tip:** Make sure your site configuration options enables the built-in icons.") 116 | .margin(.bottom, .xLarge) 117 | 118 | Alert { 119 | Text("In case you hadn't noticed, this page uses a custom theme that places some random content on the right-hand side.") 120 | } 121 | .role(.info) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /Content/article/swift-against-humanity.md: -------------------------------------------------------------------------------- 1 | --- 2 | author: Paul Hudson 3 | description: 4 | date: 2024-03-06 15:30 5 | tags: April Fools, Swift 6 | published: true 7 | image: /images/photos/washing.jpg 8 | --- 9 | # Now available to pre-order: Swift Against Humanity 10 | 11 | So you think you know Swift? Think again! Fresh from the success of our [audiobook launch](https://youtu.be/SHvBEwskO-k), Laboratoires TwoStraws is back with an all-new card game that will finally give you something to do while waiting for Xcode to finish indexing. 12 | 13 | It’s called **Swift Against Humanity**, and the rules are both dazzlingly simple and also surprisingly similar to some other games you might have previously heard of: one player places a black card on the table that contains a question or part of a sentence, and all other players play one white card they think best completes it. The first player then shuffles the white cards, reads them out, then picks a winner – it’s literally *minutes* of fun. 14 | 15 | ![Black card: The secret to making your Swift code run fast is blank. White cards: Craig Federighi’s hair gel, some tasteful WWDC-themed cosplay, Paul Hudson’s dog army, two dozen force unwrapped optionals.](/images/sah1.jpg) 16 | 17 | Pre-orders for Swift Against Humanity start tomorrow, with the first deliveries going out in the next week or so. Plus, we’re pleased to announce three incredible editions: 18 | 19 | - Swift Against Humanity Standard Edition: for the language purists out there. 20 | - Swift Against Humanity Enterprise Edition: for all you folks busy building with [Swift Enterprise Edition](https://www.hackingwithswift.com/articles/183/apple-announces-swift-enterprise-edition). 21 | - Humanity Swift Concurrency Against Edition: for all developers who just love adding concurrency in places it really doesn’t belong. 22 | 23 | ![Black card: What’s the most difficult thing about learning Swift? White cards: Always googling Swift and getting Taylor Swift results, converting a string to an array then back to a string then questioning your life choices, desperately trying to remember which button does what in Interface Builder, accidentally creating a black hole with Xcode’s Auto Layout constraints.](/images/sah2.jpg) 24 | 25 | Swift Against Humanity was developed in conjunction with legendary game designer [Daniel Leivers](https://twitter.com/sofaracing), who had this to say: “Apple Store Genius Bar Fight Club! Clippy, but for Xcode! Fantasizing about the day Swift Evolution brings back Objective-C! I can literally write anything on these cards and people will buy them, we’re going to be so rich! Wait… are you recording this?” 26 | 27 | ![Black card: Apple’s newest Swift tutorial unexpectedly features a lesson on blank. White cards: Building an iMac with wheels, Tim Cook’s surprise rap career, trying to decipher closure syntax without crying, Core Data: when you need to store data, but also test your ability to withstand migraines.](/images/sah3.jpg) 28 | 29 | We’ve been busy testing Swift Against Humanity for months now. Even Ed Kermenek, the Swift project lead, took a few hours out of his busy schedule to try it out, and had this to say: “Swift Against Humanity is neat and all, but the most important thing is that it doesn’t have any [****ing orange](https://www.hackingwithswift.com/articles/248/swift-core-team-to-swift-bloggers-please-for-the-love-of-all-things-holy-find-a-different-color-than-orange).” 30 | 31 | ![Black card: What new feature will Apple announce at this year’s WWDC? White cards: A sentient UIButton, the official Swift coding soundtrack, a support group for developers who are traumatized by Swift strings, doggo-driven development.](/images/sah4.jpg) 32 | 33 | Now at this point I know what you’re thinking: surely a game this great must attract a premium price tag? Well, you’ll be pleased to know that all three editions of Swift Against Humanity are available for just [5 SwiftCoin](https://www.hackingwithswift.com/articles/64/how-to-cut-swift-compile-times-by-half) plus tax and delivery. 34 | 35 | Plus if you order now we’ll send you an exclusive [Spot the Swifty](https://www.hackingwithswift.com/articles/215/spot-the-swifty) poster plus four limited edition bonus white cards: 36 | 37 | - “When your app gets rejected for a feature that's in 100 other apps.” 38 | - “Longing for the days of UIKit's warm embrace.” 39 | - “Manually managing memory like it's 2010.” 40 | - “Using integers to index into strings the way god intended.” 41 | 42 | ![Black card: As any Swift developer knows, the hardest thing we face is blank. White cards: Playing pin the tail on the guideline violation with App Review, reenacting Groundhog Day with Xcode provisioning profiles, the moment when you realize that radar you filed might outlive you, that one coworker who won’t stop using AnyView](/images/sah5.jpg) 43 | 44 | So, the next time Xcode tells you to try breaking up an expression into distinct sub-expressions, try breaking out Swift Against Humanity instead! 45 | -------------------------------------------------------------------------------- /Sources/Pages/Examples/LinkExamples.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LinkExamples.swift 3 | // IgniteSamples 4 | // https://www.github.com/twostraws/Ignite 5 | // See LICENSE for license information. 6 | // 7 | 8 | import Foundation 9 | import Ignite 10 | 11 | struct LinkExamples: StaticPage { 12 | var title = "Links" 13 | var description = """ 14 | Create internal and external links with customizable styling, roles, and \ 15 | button appearances. Open links in new tabs and target specific pages directly. 16 | """ 17 | 18 | var body: some HTML { 19 | Text("Links") 20 | .font(.title1) 21 | 22 | Text("Links direct users to other parts of your site, or to external locations.") 23 | .font(.lead) 24 | 25 | Text("In the simplest case we can create a link with a string target:") 26 | 27 | CodeBlock(.swift) { 28 | """ 29 | Text { 30 | Link("Go Home", target: "/") 31 | } 32 | """ 33 | } 34 | 35 | Text { 36 | Link("Go Home", target: "/") 37 | } 38 | 39 | Text("Alternatively, you can use page types directly to get their path:") 40 | 41 | CodeBlock(.swift) { 42 | """ 43 | Text { 44 | Link("Learn about carousels", target: CarouselExamples()) 45 | } 46 | """ 47 | } 48 | 49 | Text { 50 | Link("Learn about carousels", target: CarouselExamples()) 51 | } 52 | 53 | Text(markdown: "Using a `.target(.blank)` modifier opens new tabs:") 54 | 55 | CodeBlock(.swift) { 56 | """ 57 | Text { 58 | Link("Another tab of links", target: LinkExamples()) 59 | .target(.blank) 60 | } 61 | """ 62 | } 63 | 64 | Text { 65 | Link("Another tab of links", target: LinkExamples()) 66 | .target(.blank) 67 | } 68 | 69 | Alert { 70 | Text(markdown: "**Tip:** Page targets use initialized pages, so you can pass in custom values that adjust the path.") 71 | } 72 | .role(.info) 73 | 74 | Text(markdown: "Use `LinkGroup` when you need to make multiple `HTML` elements clickable as a single link:") 75 | 76 | CodeBlock(.swift) { 77 | """ 78 | LinkGroup(target: "/") { 79 | Section { 80 | Text("Building Modern Websites") 81 | .font(.title3) 82 | 83 | Text("Learn how to create beautiful, responsive interfaces using Ignite's declarative syntax.") 84 | .font(.lead) 85 | } 86 | .border(.lightGray) 87 | .padding() 88 | .cornerRadius(5) 89 | .frame(maxWidth: 500) 90 | } 91 | """ 92 | } 93 | 94 | LinkGroup(target: "/") { 95 | Section { 96 | Text("Building Modern Websites") 97 | .font(.title3) 98 | 99 | Text("Learn how to create beautiful, responsive interfaces using Ignite's declarative syntax.") 100 | .font(.lead) 101 | } 102 | .border(.lightGray) 103 | .padding() 104 | .cornerRadius(5) 105 | .frame(maxWidth: 500) 106 | } 107 | .margin(.bottom, .xLarge) 108 | 109 | Text("Link styling") 110 | .font(.title2) 111 | .margin(.top, .xLarge) 112 | 113 | Text("Links can have roles to control how they appear:") 114 | 115 | CodeBlock(.swift) { 116 | """ 117 | ForEach(Role.standardRoles) { role in 118 | Text { 119 | Link("Link with \\(role.rawValue) role.", target: "#") 120 | .role(role) 121 | } 122 | } 123 | """ 124 | } 125 | 126 | ForEach(Role.standardRoles) { role in 127 | Text { 128 | Link("Link with \(role.rawValue) role.", target: "#") 129 | .role(role) 130 | } 131 | } 132 | 133 | Text("Links as buttons") 134 | .font(.title2) 135 | .margin(.top, .xLarge) 136 | 137 | Text(markdown: "Use `linkStyle(.button)` to style links as buttons:") 138 | 139 | CodeBlock(.swift) { 140 | """ 141 | ForEach(Role.standardRoles) { role in 142 | Text { 143 | Link("Button-style link with \\(role.rawValue) role.", target: "#") 144 | .linkStyle(.button) 145 | .role(role) 146 | } 147 | } 148 | """ 149 | } 150 | 151 | ForEach(Role.standardRoles) { role in 152 | Text { 153 | Link("Button-style link with \(role.rawValue) role.", target: "#") 154 | .linkStyle(.button) 155 | .role(role) 156 | } 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /Sources/Pages/Examples/CarouselExamples.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CarouselExamples.swift 3 | // IgniteSamples 4 | // https://www.github.com/twostraws/Ignite 5 | // See LICENSE for license information. 6 | // 7 | 8 | import Foundation 9 | import Ignite 10 | 11 | struct CarouselExamples: StaticPage { 12 | var title = "Carousels" 13 | var description = """ 14 | Create dynamic image slideshows with optional content overlays. \ 15 | Design carousels with different transition styles, \ 16 | background opacity controls, and rich content layouts. \ 17 | Supports automatic scaling and cropping of images to fill available space. 18 | """ 19 | 20 | var body: some HTML { 21 | Text("Carousels") 22 | .font(.title1) 23 | 24 | Text(""" 25 | Carousels provide slideshows of images, optionally with content overlaid. \ 26 | Images scale to fill available space, even if that means being cropped, so they work best as illustrations. 27 | """) 28 | .font(.lead) 29 | 30 | Text("At their simplest, a carousel might just be a slideshow of pictures like this one:") 31 | 32 | CodeBlock(.swift) { 33 | """ 34 | Carousel { 35 | Slide(background: "/images/photos/stack.jpg") 36 | Slide(background: "/images/photos/wind.jpg") 37 | Slide(background: "/images/photos/washing.jpg") 38 | } 39 | """ 40 | } 41 | 42 | Carousel { 43 | Slide(background: "/images/photos/stack.jpg") 44 | Slide(background: "/images/photos/wind.jpg") 45 | Slide(background: "/images/photos/washing.jpg") 46 | } 47 | .margin(.bottom, .xLarge) 48 | 49 | Text(markdown: "You can adjust how carousels move from slide to slide using the `carouselStyle()` modifier:") 50 | 51 | CodeBlock(.swift) { 52 | """ 53 | Carousel { 54 | Slide(background: "/images/photos/stack.jpg") 55 | Slide(background: "/images/photos/wind.jpg") 56 | Slide(background: "/images/photos/washing.jpg") 57 | } 58 | .carouselStyle(.crossfade(3, curve: .easeInOut)) 59 | """ 60 | } 61 | 62 | Carousel { 63 | Slide(background: "/images/photos/stack.jpg") 64 | Slide(background: "/images/photos/wind.jpg") 65 | Slide(background: "/images/photos/washing.jpg") 66 | } 67 | .carouselStyle(.crossfade(3, curve: .easeInOut)) 68 | .margin(.bottom, .xLarge) 69 | 70 | Text(""" 71 | You can dim background images by setting their opacity to a value lower than 1. \ 72 | This is particularly effective when overlaying content: 73 | """) 74 | 75 | CodeBlock(.swift) { 76 | """ 77 | Carousel { 78 | Slide(background: "/images/photos/stack.jpg") { 79 | Text("This is serious.") 80 | .font(.title2) 81 | 82 | Text("This is important information about the first slide.") 83 | .font(.lead) 84 | 85 | Text { 86 | Link("Go Home", target: "/") 87 | .linkStyle(.button) 88 | } 89 | } 90 | .backgroundOpacity(0.2) 91 | 92 | Slide(background: "/images/photos/wind.jpg") { 93 | Text("Another great point.") 94 | .font(.title2) 95 | 96 | Text("This is a really convincing point to drive home how awesome carousels are.") 97 | .font(.lead) 98 | } 99 | .backgroundOpacity(0.2) 100 | 101 | Slide(background: "/images/photos/washing.jpg") { 102 | Text(markdown: "One more. *Boom*.") 103 | .font(.title2) 104 | 105 | Text("Slides, images, text – these aren't three separate things. Are you getting it?") 106 | } 107 | .backgroundOpacity(0.2) 108 | } 109 | """ 110 | } 111 | 112 | Carousel { 113 | Slide(background: "/images/photos/stack.jpg") { 114 | Text("This is serious.") 115 | .font(.title2) 116 | 117 | Text("This is important information about the first slide.") 118 | .font(.lead) 119 | 120 | Text { 121 | Link("Go Home", target: "/") 122 | .linkStyle(.button) 123 | } 124 | } 125 | .backgroundOpacity(0.2) 126 | 127 | Slide(background: "/images/photos/wind.jpg") { 128 | Text("Another great point.") 129 | .font(.title2) 130 | 131 | Text("This is a really convincing point to drive home how awesome carousels are.") 132 | .font(.lead) 133 | } 134 | .backgroundOpacity(0.2) 135 | 136 | Slide(background: "/images/photos/washing.jpg") { 137 | Text(markdown: "One more. *Boom*.") 138 | .font(.title2) 139 | 140 | Text("Slides, images, text – these aren't three separate things. Are you getting it?") 141 | } 142 | .backgroundOpacity(0.2) 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /Sources/Pages/Examples/CodeExamples.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodeExamples.swift 3 | // IgniteSamples 4 | // https://www.github.com/twostraws/Ignite 5 | // See LICENSE for license information. 6 | // 7 | 8 | import Foundation 9 | import Ignite 10 | 11 | struct CodeExamples: StaticPage { 12 | var title = "Code" 13 | var description = """ 14 | Display code snippets with syntax highlighting and language-specific formatting. \ 15 | Create inline code with Code or multi-line blocks with CodeBlock, \ 16 | supporting multiple programming languages. 17 | """ 18 | 19 | var body: some HTML { 20 | Text("Code") 21 | .font(.title1) 22 | 23 | Section { 24 | Text(markdown: "Code comes in two forms: `Code` is for inline code snippets, and `CodeBlock` for larger chunks.") 25 | .font(.lead) 26 | 27 | Text(markdown: "You can also create code with Markdown, alongside other text: `NavigationSplitView`.") 28 | 29 | Text(markdown: """ 30 | When using `CodeBlock`, you can specify a language for your code. \ 31 | If you also include that language in your site's syntax highlighters, that code will be colored automatically. 32 | """) 33 | 34 | Text("For example, here's some Swift code:") 35 | 36 | CodeBlock(.swift) { 37 | """ 38 | CodeBlock(.swift) { 39 | \""" 40 | struct ContentView: View { 41 | var body: some View { 42 | Text("Hello, Swift!") 43 | } 44 | } 45 | \""" 46 | } 47 | """ 48 | } 49 | 50 | CodeBlock(.swift) { 51 | """ 52 | struct ContentView: View { 53 | var body: some View { 54 | Text("Hello, Swift!") 55 | } 56 | } 57 | """ 58 | } 59 | .margin(.bottom, .xLarge) 60 | } 61 | 62 | Text("Here's some Python:") 63 | 64 | CodeBlock(.python) { 65 | """ 66 | CodeBlock(.python) { 67 | \""" 68 | def find_primes_up_to_100(): 69 | primes = [] 70 | for possible_prime in range(2, 101): 71 | is_prime = True 72 | for num in range(2, int(possible_prime ** 0.5) + 1): 73 | if possible_prime % num == 0: 74 | is_prime = False 75 | break 76 | if is_prime: 77 | primes.append(possible_prime) 78 | return primes 79 | \""" 80 | } 81 | """ 82 | } 83 | 84 | CodeBlock(.python) { 85 | """ 86 | def find_primes_up_to_100(): 87 | primes = [] 88 | for possible_prime in range(2, 101): 89 | is_prime = True 90 | for num in range(2, int(possible_prime ** 0.5) + 1): 91 | if possible_prime % num == 0: 92 | is_prime = False 93 | break 94 | if is_prime: 95 | primes.append(possible_prime) 96 | return primes 97 | """ 98 | } 99 | .margin(.bottom, .xLarge) 100 | 101 | Text("And here's some Ruby for good luck:") 102 | 103 | CodeBlock(.swift) { 104 | """ 105 | CodeBlock(.ruby) { 106 | \""" 107 | def print_mean_average(numbers) 108 | sum = numbers.sum 109 | count = numbers.length 110 | mean = count > 0 ? (sum.to_f / count) : 0 111 | puts "The mean average is: #{mean}" 112 | end 113 | \""" 114 | } 115 | """ 116 | } 117 | 118 | CodeBlock(.ruby) { 119 | """ 120 | def print_mean_average(numbers) 121 | sum = numbers.sum 122 | count = numbers.length 123 | mean = count > 0 ? (sum.to_f / count) : 0 124 | puts "The mean average is: #{mean}" 125 | end 126 | """ 127 | } 128 | .margin(.bottom, .xLarge) 129 | 130 | Alert { 131 | Text(markdown: "**Important:** When using syntax highlighting in articles, your site's `syntaxHighlighterConfiguration` must include those languages.") 132 | } 133 | .role(.warning) 134 | 135 | Text(markdown: "Use the `highlightedLines()` and `lineNumberVisibility()` modifiers to style your code blocks:") 136 | 137 | CodeBlock(.swift) { 138 | """ 139 | CodeBlock(.swift) { 140 | \""" 141 | struct User { 142 | let name: String 143 | let age: Int 144 | let email: String 145 | let isActive: Bool 146 | } 147 | \""" 148 | } 149 | .highlightedLines(2, 4) 150 | .lineNumberVisibility(.visible) 151 | """ 152 | } 153 | 154 | CodeBlock(.swift) { 155 | """ 156 | struct User { 157 | let name: String 158 | let age: Int 159 | let email: String 160 | let isActive: Bool 161 | } 162 | """ 163 | } 164 | .highlightedLines(2, 4) 165 | .lineNumberVisibility(.visible) 166 | .margin(.bottom, .xLarge) 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /Sources/Pages/Examples/ButtonExamples.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ButtonExamples.swift 3 | // IgniteSamples 4 | // https://www.github.com/twostraws/Ignite 5 | // See LICENSE for license information. 6 | // 7 | 8 | import Foundation 9 | import Ignite 10 | 11 | struct ButtonExamples: StaticPage { 12 | var title = "Buttons" 13 | var description = """ 14 | Create interactive buttons with semantic styling and client-side actions. \ 15 | Design buttons with different roles, sizes, and behaviors to trigger \ 16 | JavaScript events, show alerts, toggle content visibility, or navigate between pages. 17 | """ 18 | 19 | var body: some HTML { 20 | Text("Buttons") 21 | .font(.title1) 22 | 23 | Text("Badges are tappable elements on your page that can run various actions when pressed, optionally with a role attached to add styling.") 24 | .font(.lead) 25 | 26 | Text("For example, here's a primary button that shows an alert when pressed:") 27 | 28 | CodeBlock(.swift) { 29 | """ 30 | Text { 31 | Button("Say Hello") { 32 | ShowAlert(message: "Hello, world!") 33 | } 34 | .role(.primary) 35 | } 36 | """ 37 | } 38 | 39 | Text { 40 | Button("Say Hello") { 41 | ShowAlert(message: "Hello, world!") 42 | } 43 | .role(.primary) 44 | } 45 | .margin(.bottom, .xLarge) 46 | 47 | Text("Button actions can contain multiple commands, all of which get compiled to JavaScript when your site is built.") 48 | 49 | Text("For example, this code shows two pieces of text, and buttons that toggle between them:") 50 | 51 | CodeBlock(.swift) { 52 | """ 53 | Text { 54 | Button("Show First Text") { 55 | ShowElement("FirstText") 56 | HideElement("SecondText") 57 | } 58 | .role(.primary) 59 | } 60 | 61 | Text { 62 | Button("Show Second Text") { 63 | HideElement("FirstText") 64 | ShowElement("SecondText") 65 | } 66 | .role(.secondary) 67 | } 68 | 69 | Text("This is the first text.") 70 | .font(.title3) 71 | .id("FirstText") 72 | 73 | Text("This is the second text.") 74 | .font(.title3) 75 | .id("SecondText") 76 | .hidden() 77 | """ 78 | } 79 | 80 | Text { 81 | Button("Show First Text") { 82 | ShowElement("FirstText") 83 | HideElement("SecondText") 84 | } 85 | .role(.primary) 86 | } 87 | 88 | Text { 89 | Button("Show Second Text") { 90 | HideElement("FirstText") 91 | ShowElement("SecondText") 92 | } 93 | .role(.secondary) 94 | } 95 | 96 | Text("This is the first text.") 97 | .font(.title3) 98 | .id("FirstText") 99 | 100 | Text("This is the second text.") 101 | .font(.title3) 102 | .id("SecondText") 103 | .hidden() 104 | 105 | Text(markdown: "All the same actions you can use with buttons can also be used with event handlers, such as `onClick()` or `onHover()`. For example:") 106 | .margin(.top, .xLarge) 107 | 108 | Text("Hover over this text to make a message appear below.") 109 | .font(.lead) 110 | .onHover { isHovering in 111 | if isHovering { 112 | ShowElement("HiddenMessage") 113 | } else { 114 | HideElement("HiddenMessage") 115 | } 116 | } 117 | 118 | Text("Hey, listen!") 119 | .id("HiddenMessage") 120 | .hidden() 121 | 122 | Text(markdown: "If you want your button to act as a link somewhere else, it's better to use `Link` with `.linkStyle(.button)`, like this:") 123 | .margin(.top, .xLarge) 124 | 125 | CodeBlock(.swift) { 126 | """ 127 | Text { 128 | Link("This is a link button", target: ContentExamples()) 129 | .linkStyle(.button) 130 | } 131 | """ 132 | } 133 | 134 | Text { 135 | Link("This is a link button", target: ContentExamples()) 136 | .linkStyle(.button) 137 | } 138 | 139 | Text("Styling buttons") 140 | .font(.title2) 141 | .margin(.top, .xLarge) 142 | 143 | Text(markdown: "As with other several other element types, buttons can have *roles* attached to them to customize how they look. There is also a `buttonSize()` modifier to adjust how big buttons are:") 144 | 145 | CodeBlock(.swift) { 146 | """ 147 | ForEach(Role.standardRoles) { role in 148 | ForEach(Button.Size.allCases) { size in 149 | Text { 150 | Button("\\(size.rawValue.capitalized) button with \\(role.rawValue) role") 151 | .buttonSize(size) 152 | .role(role) 153 | } 154 | } 155 | } 156 | """ 157 | } 158 | 159 | ForEach(Role.standardRoles) { role in 160 | ForEach(Button.Size.allCases) { size in 161 | Text { 162 | let description = "\(size.rawValue.capitalized) button with \(role.rawValue) role" 163 | Button(role == .close ? "" : description) 164 | .buttonSize(size) 165 | .role(role) 166 | } 167 | } 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, caste, color, religion, or sexual 11 | identity and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the overall 27 | community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or advances of 32 | any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email address, 36 | without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | [INSERT CONTACT METHOD]. 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series of 87 | actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or permanent 94 | ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within the 114 | community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.1, available at 120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 127 | [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 131 | [Mozilla CoC]: https://github.com/mozilla/diversity 132 | [FAQ]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | -------------------------------------------------------------------------------- /Sources/Pages/Examples/ListExamples.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListExamples.swift 3 | // IgniteSamples 4 | // https://www.github.com/twostraws/Ignite 5 | // See LICENSE for license information. 6 | // 7 | 8 | import Foundation 9 | import Ignite 10 | 11 | struct ListExamples: StaticPage { 12 | var title = "Lists" 13 | var description = """ 14 | Create ordered and unordered lists with customizable markers. \ 15 | Support both static items and dynamic content from arrays. 16 | """ 17 | 18 | var body: some HTML { 19 | Text("Lists") 20 | .font(.title1) 21 | 22 | Section { 23 | Text("Lists can be ordered or unordered, and you can customize their bullet styles too.") 24 | .font(.lead) 25 | 26 | Text("A simple list can be made up just of hard-coded strings, like this:") 27 | 28 | CodeBlock(.swift) { 29 | """ 30 | List { 31 | "This is a list item" 32 | "So is this" 33 | "And this" 34 | } 35 | """ 36 | } 37 | 38 | List { 39 | "This is a list item" 40 | "So is this" 41 | "And this" 42 | } 43 | .margin(.bottom, .xLarge) 44 | 45 | Text(markdown: "Alternatively, you can pass in an array like this:") 46 | 47 | CodeBlock(.swift) { 48 | """ 49 | List(User.examples) { user in 50 | user.name 51 | } 52 | """ 53 | } 54 | 55 | List(User.examples) { user in 56 | user.name 57 | } 58 | .margin(.bottom, .xLarge) 59 | 60 | Text(markdown: "Lists are unordered by default. Use the `listStyle()` modifier to change that:") 61 | 62 | CodeBlock(.swift) { 63 | """ 64 | List { 65 | "This is the first list item" 66 | "This is the second one" 67 | "And here's one more" 68 | } 69 | .listStyle(.ordered) 70 | """ 71 | } 72 | } 73 | 74 | List { 75 | "This is the first list item" 76 | "This is the second one" 77 | "And here's one more" 78 | } 79 | .listMarkerStyle(.ordered(.automatic)) 80 | .margin(.bottom, .xLarge) 81 | 82 | Text("You can customize the bullet style by adjusting the list style. For example, here are Roman numerals:") 83 | 84 | CodeBlock(.swift) { 85 | """ 86 | List { 87 | "Veni" 88 | "Vidi" 89 | "Vici" 90 | } 91 | .listStyle(.ordered(.lowerRoman)) 92 | """ 93 | } 94 | 95 | List { 96 | "Veni" 97 | "Vidi" 98 | "Vici" 99 | } 100 | .listMarkerStyle(.ordered(.lowerRoman)) 101 | .margin(.bottom, .xLarge) 102 | 103 | Text("And here is a custom style using emoji:") 104 | 105 | CodeBlock(.swift) { 106 | """ 107 | List { 108 | "The players gonna play" 109 | "Haters gonna hate" 110 | "Fakers gonna fake" 111 | } 112 | .listMarkerStyle(.custom("💃")) 113 | """ 114 | } 115 | 116 | List { 117 | "The players gonna play" 118 | "Haters gonna hate" 119 | "Fakers gonna fake" 120 | } 121 | .listMarkerStyle(.custom("💃")) 122 | 123 | Text("List Styles") 124 | .font(.title2) 125 | .margin(.top, .xLarge) 126 | 127 | Text(markdown: "Lists can be styled in different ways using the `listStyle()` modifier:") 128 | 129 | CodeBlock(.swift) { 130 | """ 131 | List { 132 | "Default list item" 133 | "Another default item" 134 | } 135 | """ 136 | } 137 | 138 | List { 139 | "Default list item" 140 | "Another default item" 141 | } 142 | .margin(.bottom, .medium) 143 | 144 | CodeBlock(.swift) { 145 | """ 146 | List { 147 | "Plain list item" 148 | "Another plain list item" 149 | } 150 | .listStyle(.plain) 151 | """ 152 | } 153 | 154 | List { 155 | "Plain list item" 156 | "Another plain list item" 157 | } 158 | .listStyle(.plain) 159 | .margin(.bottom, .xLarge) 160 | 161 | CodeBlock(.swift) { 162 | """ 163 | List { 164 | "Group list item" 165 | "Another group item" 166 | .badge(Badge("1").role(.primary)) 167 | } 168 | .listStyle(.group) 169 | """ 170 | } 171 | 172 | List { 173 | "Group list item" 174 | "Another group item" 175 | .badge(Badge("1").role(.primary)) 176 | } 177 | .listStyle(.group) 178 | .margin(.bottom, .medium) 179 | 180 | CodeBlock(.swift) { 181 | """ 182 | List { 183 | "Horizontal group item" 184 | "Another horizontal item" 185 | } 186 | .listStyle(.horizontalGroup) 187 | """ 188 | } 189 | 190 | List { 191 | "Horizontal group item" 192 | "Another Horizontal item" 193 | } 194 | .listStyle(.horizontalGroup) 195 | .margin(.bottom, .xLarge) 196 | 197 | Text("List Items") 198 | .font(.title2) 199 | .margin(.top, .xLarge) 200 | 201 | Text(markdown: "When using `List` with `listStyle(.group)`, you can add roles via `ListItem`:") 202 | 203 | CodeBlock(.swift) { 204 | """ 205 | List { 206 | ForEach(Role.standardRoles) { role in 207 | ListItem { 208 | "A simple \\(role.rawValue) list group item" 209 | } 210 | .role(role) 211 | } 212 | } 213 | .listStyle(.group) 214 | """ 215 | } 216 | 217 | List { 218 | ForEach(Role.standardRoles) { role in 219 | ListItem { "A simple \(role.rawValue) list group item" } 220 | .role(role) 221 | } 222 | } 223 | .listStyle(.group) 224 | .margin(.bottom, .xLarge) 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /Sources/Pages/Examples/AccordionExamples.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccordionExamples.swift 3 | // IgniteSamples 4 | // https://www.github.com/twostraws/Ignite 5 | // See LICENSE for license information. 6 | // 7 | 8 | import Foundation 9 | import Ignite 10 | 11 | struct AccordionExamples: StaticPage { 12 | var title = "Accordions" 13 | var description = """ 14 | Create collapsible content sections that users can expand and collapse. \ 15 | Learn how to control whether multiple sections can be open at once, \ 16 | set default open states, and style accordion titles and content. 17 | """ 18 | 19 | var body: some HTML { 20 | Text("Accordions") 21 | .font(.title1) 22 | 23 | Text("Accordions are a bit like disclosure groups: users see a group of headings, and can click on one to expand it, revealing its contents.") 24 | .font(.lead) 25 | 26 | Text("This creates a simple accordion:") 27 | 28 | CodeBlock(.swift) { 29 | """ 30 | Accordion { 31 | Item("First item") { 32 | Text("This is the first accordion item.") 33 | } 34 | 35 | Item("Second item") { 36 | Text("This is the second accordion item.") 37 | } 38 | 39 | Item("Third item") { 40 | Text("This is the third accordion item.") 41 | } 42 | } 43 | .openMode(.individual) 44 | """ 45 | } 46 | 47 | Accordion { 48 | Item("First item") { 49 | Text("This is the first accordion item.") 50 | } 51 | 52 | Item("Second item") { 53 | Text("This is the second accordion item.") 54 | } 55 | 56 | Item("Third item") { 57 | Text("This is the third accordion item.") 58 | } 59 | } 60 | .openMode(.individual) 61 | .margin(.bottom, .xLarge) 62 | 63 | Text("This accordion is set to allow multiple items to be open at a time:") 64 | 65 | CodeBlock(.swift) { 66 | """ 67 | Accordion { 68 | Item("First") { 69 | Text("This is the first accordion item.") 70 | } 71 | 72 | Item("Second") { 73 | Text("This is the second accordion item.") 74 | } 75 | 76 | Item("Third") { 77 | Text("This is the third accordion item.") 78 | } 79 | } 80 | .openMode(.all) 81 | """ 82 | } 83 | 84 | Accordion { 85 | Item("First") { 86 | Text("This is the first accordion item.") 87 | } 88 | 89 | Item("Second") { 90 | Text("This is the second accordion item.") 91 | } 92 | 93 | Item("Third") { 94 | Text("This is the third accordion item.") 95 | } 96 | } 97 | .openMode(.all) 98 | .margin(.bottom, .xLarge) 99 | 100 | Text("You can configure individual items to be open by default if you want:") 101 | 102 | CodeBlock(.swift) { 103 | """ 104 | Accordion { 105 | Item("First", startsOpen: true) { 106 | Text("This item will start open by default.") 107 | } 108 | 109 | Item("Second") { 110 | Text("This is the second accordion item.") 111 | } 112 | 113 | Item("Third") { 114 | Text("This is the third accordion item.") 115 | } 116 | } 117 | .accordionStyle(.plain) 118 | .openMode(.individual) 119 | """ 120 | } 121 | 122 | Accordion { 123 | Item("First", startsOpen: true) { 124 | Text("This item will start open by default.") 125 | } 126 | 127 | Item("Second") { 128 | Text("This is the second accordion item.") 129 | } 130 | 131 | Item("Third") { 132 | Text("This is the third accordion item.") 133 | } 134 | } 135 | .openMode(.individual) 136 | .accordionStyle(.plain) 137 | .margin(.bottom, .xLarge) 138 | 139 | Text("And you can add more complex elements and styling to your accordion titles and contents if you want:") 140 | 141 | CodeBlock(.swift) { 142 | """ 143 | Accordion { 144 | Item(startsOpen: true) { 145 | Text { 146 | Image("/images/photos/chair.jpg", description: "This is a picture of a chair, and not a dog.") 147 | .resizable() 148 | } 149 | 150 | Text("This is the first accordion item.") 151 | } header: { 152 | Emphasis { "This title is italic" } 153 | .foregroundStyle(.black) 154 | } 155 | 156 | Item(Underline { "This title is underlined." }) { 157 | Text("This is the second accordion item.") 158 | } 159 | .contentBackground(.aliceBlue) 160 | 161 | Item(Strong { "This title is bold." }) { 162 | Text("This is the third accordion item.") 163 | } 164 | } 165 | .openMode(.individual) 166 | .headerBackground(.mintCream, open: .seaGreen) 167 | .headerForegroundStyle(.darkGreen, open: .white) 168 | .borderColor(.lightSeaGreen) 169 | """ 170 | } 171 | 172 | Accordion { 173 | Item(startsOpen: true) { 174 | Text { 175 | Image("/images/photos/chair.jpg", description: "This is a picture of a chair, and not a dog.") 176 | .resizable() 177 | } 178 | 179 | Text("This is the first accordion item.") 180 | } header: { 181 | Emphasis { "This title is italic" } 182 | .foregroundStyle(.black) 183 | } 184 | 185 | Item(Underline { "This title is underlined." }) { 186 | Text("This is the second accordion item.") 187 | } 188 | .contentBackground(.aliceBlue) 189 | 190 | Item(Strong { "This title is bold." }) { 191 | Text("This is the third accordion item.") 192 | } 193 | } 194 | .openMode(.individual) 195 | .headerBackground(.mintCream, open: .seaGreen) 196 | .headerForegroundStyle(.darkGreen, open: .white) 197 | .borderColor(.lightSeaGreen) 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /Sources/Pages/Concepts/GridExamples.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridExamples.swift 3 | // IgniteSamples 4 | // https://www.github.com/twostraws/Ignite 5 | // See LICENSE for license information. 6 | // 7 | 8 | import Foundation 9 | import Ignite 10 | 11 | struct GridExamples: StaticPage { 12 | var title = "Grid Layout" 13 | var description = """ 14 | Create flexible, responsive layouts using Ignite's 12-column grid system. \ 15 | Learn how to subdivide space, set explicit widths, and handle wrapping content automatically. 16 | """ 17 | 18 | var body: some HTML { 19 | Text("Grid Layout") 20 | .font(.title1) 21 | 22 | Text(""" 23 | All layout in Ignite is handled using an invisible 12-column grid layout. \ 24 | You can subdivide those columns however you want, or let Ignite do it for you. 25 | """) 26 | .font(.lead) 27 | 28 | Text(markdown: "To create a grid, use the `Grid` element. Your columns will be subdivided to make space for each item you've placed.") 29 | 30 | Text("For example, these pictures all take up one third of the available space:") 31 | 32 | CodeBlock(.swift) { 33 | """ 34 | Grid { 35 | Image("/images/photos/shades.jpg", description: "A pair of sunglasses.") 36 | .resizable() 37 | 38 | Image("/images/photos/stack.jpg", description: "A door partly open.") 39 | .resizable() 40 | 41 | 42 | Image("/images/photos/wind.jpg", description: "A windy day.") 43 | .resizable() 44 | } 45 | """ 46 | } 47 | 48 | Grid { 49 | Image("/images/photos/shades.jpg", description: "A pair of sunglasses.") 50 | .resizable() 51 | 52 | Image("/images/photos/stack.jpg", description: "A door partly open.") 53 | .resizable() 54 | 55 | Image("/images/photos/wind.jpg", description: "A windy day.") 56 | .resizable() 57 | } 58 | .margin(.bottom, .xLarge) 59 | 60 | Text(markdown: "You can also explicitly set a width for each element using the `width()` modifier.") 61 | 62 | Text(""" 63 | This modifier automatically causes your views to rearrange themselves when horizontal space is restricted. 64 | For example, these pictures all take up one third of the available space when the browser window is wide, but all the space when the window is narrow: 65 | """) 66 | 67 | CodeBlock(.swift) { 68 | """ 69 | Grid { 70 | Image("/images/photos/shades.jpg", description: "A pair of sunglasses.") 71 | .resizable() 72 | .width(4) 73 | 74 | Image("/images/photos/stack.jpg", description: "A door partly open.") 75 | .resizable() 76 | .width(4) 77 | 78 | Image("/images/photos/wind.jpg", description: "A windy day.") 79 | .resizable() 80 | .width(4) 81 | } 82 | """ 83 | } 84 | 85 | Grid { 86 | Image("/images/photos/shades.jpg", description: "A pair of sunglasses.") 87 | .resizable() 88 | .width(4) 89 | 90 | Image("/images/photos/stack.jpg", description: "A door partly open.") 91 | .resizable() 92 | .width(4) 93 | 94 | Image("/images/photos/wind.jpg", description: "A windy day.") 95 | .resizable() 96 | .width(4) 97 | } 98 | .margin(.bottom, .xLarge) 99 | 100 | Text("Wrapping items") 101 | .font(.title1) 102 | 103 | Text(""" 104 | If you place more than 12 columns of items in a grid, they will wrap automatically. \ 105 | For example, this uses four pictures of width 4, causing one to wrap to the next line: 106 | """) 107 | 108 | CodeBlock(.swift) { 109 | """ 110 | Grid(alignment: .leading) { 111 | Image("/images/photos/shades.jpg", description: "A pair of sunglasses.") 112 | .resizable() 113 | .width(4) 114 | 115 | Image("/images/photos/stack.jpg", description: "A door partly open.") 116 | .resizable() 117 | .width(4) 118 | 119 | Image("/images/photos/rug.jpg", description: "A nice rug.") 120 | .resizable() 121 | .width(4) 122 | 123 | Image("/images/photos/car.jpg", description: "The window of a car.") 124 | .resizable() 125 | .width(4) 126 | } 127 | """ 128 | } 129 | 130 | Grid(alignment: .leading) { 131 | Image("/images/photos/shades.jpg", description: "A pair of sunglasses.") 132 | .resizable() 133 | .width(4) 134 | 135 | Image("/images/photos/stack.jpg", description: "A door partly open.") 136 | .resizable() 137 | .width(4) 138 | 139 | Image("/images/photos/rug.jpg", description: "A nice rug.") 140 | .resizable() 141 | .width(4) 142 | 143 | Image("/images/photos/car.jpg", description: "The window of a car.") 144 | .resizable() 145 | .width(4) 146 | } 147 | .margin(.bottom, .xLarge) 148 | 149 | Text("Although 12 columns is the default, you can adjust it downwards if needed. For example, this uses a 2-column grid:") 150 | 151 | CodeBlock(.swift) { 152 | """ 153 | Grid { 154 | Image("/images/photos/shades.jpg", description: "A pair of sunglasses.") 155 | .resizable() 156 | 157 | Image("/images/photos/stack.jpg", description: "A door partly open.") 158 | .resizable() 159 | 160 | Image("/images/photos/rug.jpg", description: "A nice rug.") 161 | .resizable() 162 | 163 | Image("/images/photos/car.jpg", description: "The window of a car.") 164 | .resizable() 165 | } 166 | .columns(2) 167 | """ 168 | } 169 | 170 | Grid { 171 | Image("/images/photos/shades.jpg", description: "A pair of sunglasses.") 172 | .resizable() 173 | 174 | Image("/images/photos/stack.jpg", description: "A door partly open.") 175 | .resizable() 176 | 177 | Image("/images/photos/rug.jpg", description: "A nice rug.") 178 | .resizable() 179 | 180 | Image("/images/photos/car.jpg", description: "The window of a car.") 181 | .resizable() 182 | } 183 | .columns(2) 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /Sources/Pages/Concepts/FormExamples.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FormExamples.swift 3 | // IgniteSamples 4 | // https://www.github.com/twostraws/Ignite 5 | // See LICENSE for license information. 6 | // 7 | 8 | import Ignite 9 | 10 | struct FormExamples: StaticPage { 11 | var title = "Forms" 12 | var description = """ 13 | Learn about Ignite's native search and newsletter subscription forms, \ 14 | or create custom forms with text fields, buttons, and control groups. 15 | """ 16 | 17 | var body: some HTML { 18 | Text("Forms") 19 | .font(.title1) 20 | 21 | Text(markdown: "Create everything from simple newsletter signups to complex data entry forms.") 22 | .font(.lead) 23 | 24 | Text("Basic form elements") 25 | .font(.title2) 26 | 27 | Text(markdown: "The `Form` element lets you create custom forms with any combination of inputs, buttons, and `FormItem`:") 28 | 29 | CodeBlock(.swift) { 30 | """ 31 | Form { 32 | TextField("Name", prompt: "Enter your name") 33 | .type(.text) 34 | .width(1) 35 | 36 | Button("Submit") 37 | .type(.submit) 38 | .role(.primary) 39 | .width(1) 40 | } 41 | .columns(2) 42 | """ 43 | } 44 | 45 | Form { 46 | TextField("Name", prompt: "Enter your name") 47 | .type(.text) 48 | .width(1) 49 | 50 | Button("Submit") 51 | .type(.submit) 52 | .role(.primary) 53 | .width(1) 54 | } 55 | .columns(2) 56 | .margin(.bottom, .xLarge) 57 | 58 | Text("Label styles") 59 | .font(.title2) 60 | 61 | Text(markdown: "Forms support three different label styles:") 62 | 63 | Text("floating") 64 | .font(.title4) 65 | .smallCaps() 66 | 67 | Text(markdown: "Floating labels animate when focused, providing a modern and space-efficient interface.") 68 | 69 | CodeBlock(.swift) { 70 | """ 71 | Form { 72 | TextField("Name", prompt: "Enter your name") 73 | .type(.text) 74 | 75 | Button("Submit") 76 | .type(.submit) 77 | .role(.primary) 78 | } 79 | .labelStyle(.floating) 80 | """ 81 | } 82 | 83 | Form { 84 | TextField("Name", prompt: "Enter your name") 85 | .type(.text) 86 | 87 | Button("Submit") 88 | .type(.submit) 89 | .role(.primary) 90 | } 91 | .labelStyle(.floating) 92 | .margin(.bottom, .large) 93 | 94 | Text("leading") 95 | .font(.title4) 96 | .smallCaps() 97 | 98 | Text(markdown: "Leading labels appear to the left of inputs, providing a traditional form layout.") 99 | 100 | CodeBlock(.swift) { 101 | """ 102 | Form { 103 | TextField("Name", prompt: "Enter your name") 104 | .type(.text) 105 | 106 | Button("Submit") 107 | .type(.submit) 108 | .role(.primary) 109 | } 110 | .labelStyle(.leading) 111 | """ 112 | } 113 | 114 | Form { 115 | TextField("Name", prompt: "Enter your name") 116 | .type(.text) 117 | 118 | Button("Submit") 119 | .type(.submit) 120 | .role(.primary) 121 | } 122 | .labelStyle(.leading) 123 | .margin(.bottom, .large) 124 | 125 | Text("hidden") 126 | .font(.title4) 127 | .smallCaps() 128 | 129 | Text(markdown: "Hidden labels are accessible to screen readers but not visible, creating a clean interface.") 130 | 131 | CodeBlock(.swift) { 132 | """ 133 | Form { 134 | TextField("Name", prompt: "Enter your name") 135 | .type(.text) 136 | 137 | Button("Submit") 138 | .type(.submit) 139 | .role(.primary) 140 | } 141 | .labelStyle(.hidden) 142 | """ 143 | } 144 | 145 | Form { 146 | TextField("Name", prompt: "Enter your name") 147 | .type(.text) 148 | 149 | Button("Submit") 150 | .type(.submit) 151 | .role(.primary) 152 | } 153 | .labelStyle(.hidden) 154 | .margin(.bottom, .large) 155 | 156 | Text("Control groups") 157 | .font(.title2) 158 | 159 | Text(markdown: "Use `ControlGroup` to combine related form elements into a single visual unit:") 160 | 161 | CodeBlock(.swift) { 162 | """ 163 | Form { 164 | ControlGroup("Username") { 165 | Span("@") 166 | TextField("Username", prompt: "Choose a username") 167 | } 168 | 169 | ControlGroup("Password") { 170 | TextField("Password", prompt: "Enter your password") 171 | .type(.password) 172 | Button("Unlock", systemImage: "key-fill") 173 | .role(.primary) 174 | } 175 | } 176 | """ 177 | } 178 | 179 | Form { 180 | ControlGroup("Username") { 181 | Span("@") 182 | TextField("Username", prompt: "Choose a username") 183 | } 184 | 185 | ControlGroup("Password") { 186 | TextField("Password", prompt: "Enter your password") 187 | .type(.password) 188 | Button("Unlock", systemImage: "key-fill") 189 | .role(.primary) 190 | } 191 | } 192 | .margin(.bottom, .xLarge) 193 | 194 | Text("Built-in Forms") 195 | .font(.title2) 196 | 197 | Text(markdown: "Ignite includes specialized forms for common use cases like newsletter subscriptions.") 198 | 199 | Text("Newsletter subscriptions") 200 | .font(.title3) 201 | 202 | Text(markdown: "The `SubscribeForm` makes it easy to collect email addresses for newsletters:") 203 | 204 | CodeBlock(.swift) { 205 | """ 206 | SubscribeForm(.buttondown("yourusername")) 207 | .emailFieldLabel("Your email address") 208 | .subscribeButtonLabel("Join our newsletter") 209 | .formStyle(.stacked) 210 | """ 211 | } 212 | 213 | SubscribeForm(.buttondown("yourusername")) 214 | .emailFieldLabel("Your email address") 215 | .subscribeButtonLabel("Join our newsletter") 216 | .formStyle(.stacked) 217 | .margin(.bottom, .xLarge) 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /Sources/Pages/Concepts/StylingExamples.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StylingExamples.swift 3 | // IgniteSamples 4 | // https://www.github.com/twostraws/Ignite 5 | // See LICENSE for license information. 6 | // 7 | 8 | import Foundation 9 | import Ignite 10 | 11 | struct StylingExamples: StaticPage { 12 | var title = "Styling" 13 | var path = "/custom/path/to/styling" 14 | var description = """ 15 | Learn how to style your content with colors, shadows, and positioning. \ 16 | See examples of opacity, tooltips, sticky positioning, and the \ 17 | important difference between margins and padding. 18 | """ 19 | 20 | var body: some HTML { 21 | Text("Styling") 22 | .font(.title1) 23 | 24 | Text("There are lots of ways you can style your page content; this page demonstrates some examples.") 25 | .class("Wom") 26 | .font(.lead) 27 | 28 | Grid(alignment: .top) { 29 | Section { 30 | Text("Here's an image with a fixed width and 50% opacity:") 31 | 32 | CodeBlock(.swift) { 33 | """ 34 | Image("/images/photos/dishwasher.jpg", description: "A dishwasher, and not a dog.") 35 | .resizable() 36 | .frame(width: 300) 37 | .opacity(0.5) 38 | """ 39 | } 40 | 41 | Image("/images/photos/dishwasher.jpg", description: "A dishwasher, and not a dog.") 42 | .resizable() 43 | .frame(width: 300) 44 | .opacity(0.5) 45 | .margin(.bottom, .xLarge) 46 | 47 | Text("The following heading has a background color, foreground style, and inner shadow:") 48 | 49 | CodeBlock(.swift) { 50 | """ 51 | Text("Hello, world!") 52 | .font(.title2) 53 | .padding(20) 54 | .foregroundStyle(.white) 55 | .background(.indigo) 56 | .innerShadow(.black, radius: 20) 57 | """ 58 | } 59 | 60 | Text("Hello, world!") 61 | .font(.title2) 62 | .padding(20) 63 | .foregroundStyle(.white) 64 | .background(.indigo) 65 | .innerShadow(.black, radius: 20) 66 | .margin(.bottom, .xLarge) 67 | 68 | Text("The next heading has a tooltip – hover over it to see what it says:") 69 | 70 | CodeBlock(.swift) { 71 | """ 72 | Text { 73 | Span("Hover over me") 74 | .hint(markdown: "Why, *hello* there!") 75 | } 76 | .font(.title2) 77 | """ 78 | } 79 | 80 | Text { 81 | Span("Hover over me") 82 | .hint(markdown: "Why, *hello* there!") 83 | } 84 | .font(.title2) 85 | } 86 | .width(7) 87 | .margin(.bottom, .xLarge) 88 | 89 | Section { 90 | CodeBlock(.swift) { 91 | """ 92 | Section { 93 | Text("This section is sticky. Try scrolling down!") 94 | 95 | Image("/images/photos/washing.jpg", description: "A laundry basket.") 96 | .resizable() 97 | } 98 | .position(.stickyTop) 99 | .padding(.top, 80) 100 | """ 101 | } 102 | 103 | Section { 104 | Text("This section is sticky. Try scrolling down!") 105 | 106 | Image("/images/photos/washing.jpg", description: "A laundry basket.") 107 | .resizable() 108 | } 109 | } 110 | .width(5) 111 | .position(.stickyTop) 112 | .padding(.top, 80) 113 | } 114 | 115 | Spacer(size: 300) 116 | 117 | Text("This space left intentionally blank.") 118 | 119 | Spacer(size: 300) 120 | 121 | Text("The section on the right started in its original location, but when it reached the top it stuck there until you scrolled past the end of its section, at which point it scrolled again.") 122 | 123 | CodeBlock(.swift) { 124 | """ 125 | Text(markdown: "Get this in your own content using `.position(.stickyTop)`.") 126 | """ 127 | } 128 | 129 | Text(markdown: "Get this in your own content using `.position(.stickyTop)`.") 130 | .margin(.bottom, .xLarge) 131 | 132 | Text("A note about margin and padding") 133 | .font(.title2) 134 | 135 | Text(markdown: """ 136 | One significant difference between SwiftUI and Ignite is the `padding()` modifier. \ 137 | On the web we distinguish between *margin*, which is the space between this element and the next one, and *padding*, which is space inside the element. 138 | """) 139 | 140 | Text("To see the difference in action, the heading below has no padding and 100 pixels of margins on all sides:") 141 | 142 | CodeBlock(.swift) { 143 | """ 144 | Text("Zero padding, lots of margin") 145 | .padding(.none) 146 | .margin(100) 147 | .background(.purple) 148 | .foregroundStyle(.white) 149 | """ 150 | } 151 | 152 | Text("Zero padding, lots of margin") 153 | .padding(.none) 154 | .margin(100) 155 | .background(.purple) 156 | .foregroundStyle(.white) 157 | 158 | Text("Notice how it's set away from surrounding context, but the purple background fits tightly around it.") 159 | 160 | Text("In comparison, the heading below has 100 pixels of padding on all sides and no margins:") 161 | 162 | CodeBlock(.swift) { 163 | """ 164 | Text("Lots of padding, zero margin") 165 | .padding(100) 166 | .margin(.none) 167 | .background(.purple) 168 | .foregroundStyle(.white) 169 | """ 170 | } 171 | 172 | Text("Lots of padding, zero margin") 173 | .padding(100) 174 | .margin(.none) 175 | .background(.purple) 176 | .foregroundStyle(.white) 177 | 178 | Text(markdown: "This time the heading has lots of purple space around it – the heading has had extra space added *inside it*, which is colored. However, it runs edge to edge otherwise; this text sits directly below it.") 179 | 180 | Text(markdown: "Although Ignite could easily have followed SwiftUI's meaning of `padding()`, it would cause more problems than it solves because it wouldn't match the way the web works.") 181 | 182 | Alert { 183 | Text(markdown: """ 184 | **PS:** Did you notice the URL for this page? \ 185 | Static layouts have their URLs generated from their type name, but you can also provide a custom path. \ 186 | If you're using `Link` with a `StaticPage` target it will ✨Just Work✨. 187 | """) 188 | } 189 | .role(.info) 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /Sources/Pages/Concepts/ContentExamples.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentExamples.swift 3 | // IgniteSamples 4 | // https://www.github.com/twostraws/Ignite 5 | // See LICENSE for license information. 6 | // 7 | 8 | import Foundation 9 | import Ignite 10 | 11 | struct ContentExamples: StaticPage { 12 | @Environment(\.articles) var articles 13 | var title = "Content" 14 | var description = """ 15 | Organize and display Markdown content with YAML front matter, article previews, and tag pages. \ 16 | Create flexible layouts and access content through the @Environment. 17 | """ 18 | 19 | var body: some HTML { 20 | Text("Working with content") 21 | .font(.title1) 22 | 23 | Text("Ignite parses Markdown files in in your Content folder, automatically converting them to pages.") 24 | .font(.lead) 25 | 26 | Text(""" 27 | You should create subfolders inside Content to represent different types of articles. \ 28 | This might be by date, e.g. Content/2023, Content/2024, etc, by type, e.g. Content/posts, Content/tutorials, or whatever other approach you want. 29 | """) 30 | 31 | Text("When it come to rendering your articles, you have two options:") 32 | 33 | List { 34 | "You can use YAML front matter to specify the layout to use for your article." 35 | "You can provide only one layout in your site, and it will be used if nothing else is specified." 36 | } 37 | .listMarkerStyle(.ordered(.automatic)) 38 | 39 | Text(markdown: """ 40 | This example site contains two types of articles, posts and stories. Some articles requests a specific layout, but others don't. 41 | """) 42 | 43 | Text(markdown: "[This article uses the default layout](/story/luna-and-arya-come-to-wwdc).") 44 | 45 | Text(markdown: "[And this article uses a custom layout](/story/sam-swift-saves-the-day).") 46 | 47 | Alert { 48 | Text(markdown: "**Important**: When adding article layouts to your `Site`, make sure to explicitly define the type of `articlePages` as `[any ArticlePage]`. Otherwise the compiler may infer the wrong type:") 49 | CodeBlock(.swift) { 50 | """ 51 | var articlePages: [any ArticlePage] = [MyBlogLayout()] 52 | """ 53 | } 54 | } 55 | .role(.warning) 56 | .margin(.bottom, .xLarge) 57 | 58 | Text("Listing articles") 59 | .font(.title2) 60 | 61 | Text(markdown: "You can access Markdown content by adding the property `@Environment(\\.articles) var articles` to your view, which lets you read all articles, or articles with a specific type or tag.") 62 | 63 | Text("For example, we can write code to show a list of all articles right here on this page:") 64 | 65 | CodeBlock(.swift) { 66 | """ 67 | List { 68 | ForEach(articles.all) { article in 69 | Link(article) 70 | } 71 | } 72 | """ 73 | } 74 | 75 | List { 76 | ForEach(articles.all) { article in 77 | Link(article) 78 | } 79 | } 80 | 81 | Text(markdown: "Or we could show only articles that matches the type `story`:") 82 | 83 | CodeBlock(.swift) { 84 | """ 85 | List { 86 | ForEach(articles.typed("story")) { article in 87 | Link(article) 88 | } 89 | } 90 | """ 91 | } 92 | 93 | List { 94 | ForEach(articles.typed("story")) { article in 95 | Link(article) 96 | } 97 | } 98 | 99 | Text("But there are a handful of helpers available to make things both easier and more attractive.") 100 | 101 | Text(markdown: "First, `ArticlePreview` can be used to make a preview for articles. This automatically includes the articles image, title, description, link, and tags, all in one:") 102 | 103 | CodeBlock(.swift) { 104 | """ 105 | Grid(articles.all, alignment: .top) { item in 106 | ArticlePreview(for: item) 107 | .width(3) 108 | .margin(.bottom) 109 | } 110 | """ 111 | } 112 | 113 | Grid(articles.all, alignment: .top) { item in 114 | ArticlePreview(for: item) 115 | .width(3) 116 | .margin(.bottom) 117 | } 118 | .margin(.bottom, .xLarge) 119 | 120 | Text("YAML front matter") 121 | .font(.title1) 122 | 123 | Text(markdown: "Ignite supports [Jekyll-style](https://jekyllrb.com/docs/front-matter/) YAML front matter to specify metadata.") 124 | Text("Specifically, the following fields are supported:") 125 | 126 | List { 127 | Text(markdown: "`author`: The author's name or contact details, depending on what you want.") 128 | Text(markdown: "`date`: Set to an ISO-8601 date for when this article was first published.") 129 | Text(markdown: "`image`: The main image for this content. Will be used with content previews and social sharing.") 130 | Text(markdown: "`imageDescription`: Screenreader-friendly text describing your article's main image.") 131 | Text(markdown: "`lastModified`: Set to an ISO-8601 date for when this article was last changed. If this is not present, the published date will be used instead.") 132 | Text(markdown: "`layout`: The name of the `ArticlePage` struct in your site to use for this content.") 133 | Text(markdown: "`published`: Set to false to leave this article unpublished on your site.") 134 | Text(markdown: "`subtitle`: An optional subheading for your content.") 135 | Text(markdown: "`tags`: A comma-separated list of tags for this article. If you have enabled tag pages, these will be used to list matching content.") 136 | } 137 | 138 | Text(markdown: "As well as the predefined fields, you can use the `metadata` dictionary to access any custom properties you have defined in the front matter. Note that the dictionary values are optionals: your page code must be able to deal with the dictionary item not existing!") 139 | 140 | CodeBlock(.swift) { 141 | """ 142 | Text(article.metadata["CustomValue"] ?? "Not defined") 143 | """ 144 | } 145 | 146 | Text(markdown: "In addition, you can read properties such as `estimatedWordCount` and `estimatedReadingMinutes` on your content, to provide extra information to users.") 147 | 148 | Text("Tag pages") 149 | .font(.title2) 150 | .margin(.top, .xLarge) 151 | 152 | Text(markdown: "If you make a type that conforms to the `TagPage` protocol, you can use it to display tag pages on your site.") 153 | 154 | Text(markdown: "This protocol passes you one of two types conforming to the `Category` protocol: `TagCategory`, which represents a page dedicated the content of a single tag, or `AllTagsCategory`, which represents a page featuring all tags and their content.") 155 | 156 | Text("This sample site has a small tags page implementation. You can see it in action with these links:") 157 | 158 | List { 159 | Link("Tag: April Fools", target: "/tags/april-fools") 160 | Link("Tag: Swift", target: "/tags/swift") 161 | Link("All tags", target: "/tags") 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /Sources/Pages/Concepts/NavigationExamples.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationExamples.swift 3 | // IgniteSamples 4 | // https://www.github.com/twostraws/Ignite 5 | // See LICENSE for license information. 6 | // 7 | 8 | import Foundation 9 | import Ignite 10 | 11 | struct NavigationExamples: StaticPage { 12 | var title = "Navigation" 13 | var description = """ 14 | Create responsive navigation bars with logos, links, and dropdowns. \ 15 | Customize with menu icons, sticky positioning, and persistent \ 16 | action controls that stay visible on mobile. 17 | """ 18 | 19 | var body: some HTML { 20 | Text("Navigation") 21 | .font(.title1) 22 | 23 | Text(markdown: "The `NavigationBar` element helps you add clear branding and navigation on every page.") 24 | .font(.lead) 25 | 26 | Text("A simple navigation bar has a title and some links, like this:") 27 | 28 | CodeBlock(.swift) { 29 | """ 30 | NavigationBar(logo: "My Awesome Site") { 31 | Link("Accordions", target: AccordionExamples()) 32 | Link("Carousels", target: CarouselExamples()) 33 | Link("Tables", target: TableExamples()) 34 | } 35 | .background(.black) 36 | .navigationBarStyle(.dark) 37 | """ 38 | } 39 | 40 | NavigationBar(logo: "My Awesome Site") { 41 | Link("Accordions", target: AccordionExamples()) 42 | Link("Carousels", target: CarouselExamples()) 43 | Link("Tables", target: TableExamples()) 44 | } 45 | .navigationBarStyle(.dark) 46 | .margin(.bottom, .large) 47 | .background(.black) 48 | 49 | Text("All navigation bars automatically adapt to a mobile-friendly layout when horizontal space is restricted. Try it out here!") 50 | 51 | Alert { 52 | Text(markdown: "If you plan to use a fixed bar like the one on this site, make sure you add some top padding to your `Body` object in your theme, so it doesn't start under the navigation bar.") 53 | } 54 | .role(.warning) 55 | .margin(.vertical, .large) 56 | 57 | Text(markdown: "You can also place `Dropdown` elements in there, like this:") 58 | 59 | CodeBlock(.swift) { 60 | """ 61 | NavigationBar(logo: "My Awesome Site") { 62 | Link("Accordions", target: AccordionExamples()) 63 | Link("Carousels", target: CarouselExamples()) 64 | Link("Tables", target: TableExamples()) 65 | 66 | Dropdown("More") { 67 | Link("Cards", target: CardExamples()) 68 | Link("Images", target: ImageExamples()) 69 | Link("Lists", target: ListExamples()) 70 | } 71 | } 72 | .navigationBarStyle(.light) 73 | .background(.antiqueWhite) 74 | """ 75 | } 76 | 77 | NavigationBar(logo: "My Awesome Site") { 78 | Link("Accordions", target: AccordionExamples()) 79 | Link("Carousels", target: CarouselExamples()) 80 | Link("Tables", target: TableExamples()) 81 | 82 | Dropdown("More") { 83 | Link("Cards", target: CardExamples()) 84 | Link("Images", target: ImageExamples()) 85 | Link("Lists", target: ListExamples()) 86 | } 87 | } 88 | .navigationBarStyle(.light) 89 | .background(.antiqueWhite) 90 | .margin(.bottom, .xLarge) 91 | 92 | Text(markdown: "Use modifiers like `navigationItemAlignment()`, `navigationMenuIcon()`, and `navigationMenuStyle()` to customize the appearance of the navigation bar:") 93 | 94 | CodeBlock(.swift) { 95 | """ 96 | NavigationBar(logo: "My Awesome Site") { 97 | Link("Accordions", target: AccordionExamples()) 98 | Link("Carousels", target: CarouselExamples()) 99 | Link("Tables", target: TableExamples()) 100 | } 101 | .navigationItemAlignment(.leading) 102 | .navigationMenuIcon(.ellipsis) 103 | .navigationMenuStyle(.plain) 104 | .background(.steelBlue) 105 | """ 106 | } 107 | 108 | NavigationBar(logo: "My Awesome Site") { 109 | Link("Accordions", target: AccordionExamples()) 110 | Link("Carousels", target: CarouselExamples()) 111 | Link("Tables", target: TableExamples()) 112 | } 113 | .navigationItemAlignment(.leading) 114 | .navigationMenuIcon(.ellipsis) 115 | .navigationMenuStyle(.plain) 116 | .background(.steelBlue) 117 | .margin(.bottom, .xLarge) 118 | 119 | Text(markdown: "Show buttons, forms, and other controls at all screen sizes using `navigationBarVisibility()`. These items will remain visible even when the navigation menu is collapsed:") 120 | 121 | CodeBlock(.swift) { 122 | """ 123 | NavigationBar(logo: "My Awesome Site") { 124 | Link("Accordions", target: AccordionExamples()) 125 | Link("Carousels", target: CarouselExamples()) 126 | Link("Tables", target: TableExamples()) 127 | 128 | Link("GitHub", target: "https://github.com/twostraws/Ignite") 129 | .target(.newWindow) 130 | .linkStyle(.button) 131 | .role(.danger) 132 | .navigationBarVisibility(.always) 133 | } 134 | .navigationBarStyle(.dark) 135 | .background(.paleVioletRed) 136 | """ 137 | } 138 | 139 | NavigationBar(logo: "My Awesome Site") { 140 | Link("Accordions", target: AccordionExamples()) 141 | Link("Carousels", target: CarouselExamples()) 142 | Link("Tables", target: TableExamples()) 143 | 144 | Link("GitHub", target: "https://github.com/twostraws/Ignite") 145 | .target(.newWindow) 146 | .linkStyle(.button) 147 | .role(.danger) 148 | .navigationBarVisibility(.always) 149 | } 150 | .navigationBarStyle(.dark) 151 | .background(.paleVioletRed) 152 | .margin(.bottom, .xLarge) 153 | 154 | Text(markdown: """ 155 | And finally, use the `position()` modifier to adjust where the bar is this placed. \ 156 | This page has a firebrick red bar fixed at the top, and a steel blue bar fixed at the bottom. 157 | """) 158 | 159 | CodeBlock(.swift) { 160 | """ 161 | NavigationBar(logo: "My Awesome Site") { 162 | Link("Accordions", target: AccordionExamples()) 163 | Link("Carousels", target: CarouselExamples()) 164 | Link("Tables", target: TableExamples()) 165 | } 166 | .navigationBarStyle(.dark) 167 | .position(.fixedBottom) 168 | .background(.steelBlue) 169 | """ 170 | } 171 | 172 | NavigationBar(logo: "My Awesome Site") { 173 | Link("Accordions", target: AccordionExamples()) 174 | Link("Carousels", target: CarouselExamples()) 175 | Link("Tables", target: TableExamples()) 176 | } 177 | .navigationBarStyle(.dark) 178 | .position(.fixedBottom) 179 | .background(.steelBlue) 180 | 181 | Text("Just remember: when you used fixed bars, it's really important to add some padding to your body so your content doesn't stay under a fixed bar.") 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /Sources/Pages/Concepts/TextExamples.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextExamples.swift 3 | // IgniteSamples 4 | // https://www.github.com/twostraws/Ignite 5 | // See LICENSE for license information. 6 | // 7 | 8 | import Foundation 9 | import Ignite 10 | 11 | struct TextExamples: StaticPage { 12 | var title = "Text" 13 | var description = """ 14 | Format text with Markdown, custom styles, and embedded elements. \ 15 | Learn how to adjust font weights, colors, alignment, and line limits \ 16 | to create beautiful typography. 17 | """ 18 | 19 | var body: some HTML { 20 | Text("Text") 21 | .font(.title1) 22 | 23 | Text(markdown: "Text can be created in a variety of ways, but the most common is putting a string into a `Text` element.") 24 | .font(.lead) 25 | 26 | CodeBlock(.swift) { 27 | """ 28 | Text(markdown: \""" 29 | Although you *can* use elements such as `Strong` and `Italic` to style your text, it's easier to use Markdown. \ 30 | To do that, use `Text(markdown:)` then write your Markdown as normal. 31 | \""") 32 | """ 33 | } 34 | 35 | Text(markdown: """ 36 | Although you *can* use elements such as `Strong` and `Italic` to style your text, it's easier to use Markdown. \ 37 | To do that, use `Text(markdown:)` then write your Markdown as normal. 38 | 39 | It's common to place other elements inside text, to gain access to alignment and more. For example, icons, badges, \ 40 | and images are all commonly used inside text. 41 | """) 42 | 43 | CodeBlock(.swift) { 44 | """ 45 | Text { 46 | "To do that, pass items into the text using a trailing closure. " 47 | 48 | Strong { 49 | "Using this approach you can add styling all you want, just by building up elements. " 50 | 51 | Emphasis { 52 | "Although Markdown is still easier, to be honest! " 53 | } 54 | } 55 | 56 | "Here's an image right inside our text:" 57 | 58 | Image("/images/photos/washing.jpg", description: "A laundry basket.") 59 | .resizable() 60 | .frame(maxWidth: 250) 61 | } 62 | """ 63 | } 64 | 65 | Text { 66 | "To do that, pass items into the text using a trailing closure. " 67 | 68 | Strong { 69 | "Using this approach you can add styling all you want, just by building up elements. " 70 | 71 | Emphasis { 72 | "Although Markdown is still easier, to be honest! " 73 | } 74 | } 75 | 76 | "Here's an image right inside our text:" 77 | 78 | Image("/images/photos/washing.jpg", description: "A laundry basket.") 79 | .resizable() 80 | .frame(maxWidth: 250) 81 | } 82 | 83 | Text("Styling your text") 84 | .font(.title2) 85 | .margin(.top, .xLarge) 86 | 87 | Text(markdown: "You can adjust the weight of text using the `fontWeight()` modifier, like this:") 88 | 89 | CodeBlock(.swift) { 90 | """ 91 | Text("Hello, world!") 92 | .font(.title3) 93 | .fontWeight(.black) 94 | """ 95 | } 96 | 97 | Text("Hello, world!") 98 | .font(.title3) 99 | .fontWeight(.black) 100 | .margin(.bottom, .xLarge) 101 | 102 | Text(markdown: "There are a variety of available weights available, from `.black` down to `.ultraThin`:") 103 | 104 | CodeBlock(.swift) { 105 | """ 106 | Text("Hello, world: Black") 107 | .font(.title3) 108 | .fontWeight(.black) 109 | 110 | Text("Hello, world: Heavy") 111 | .font(.title3) 112 | .fontWeight(.heavy) 113 | 114 | Text("Hello, world: Bold") 115 | .font(.title3) 116 | .fontWeight(.bold) 117 | 118 | Text("Hello, world: Semibold") 119 | .font(.title3) 120 | .fontWeight(.semibold) 121 | 122 | Text("Hello, world: Medium") 123 | .font(.title3) 124 | .fontWeight(.medium) 125 | 126 | Text("Hello, world: Regular") 127 | .font(.title3) 128 | .fontWeight(.regular) 129 | 130 | Text("Hello, world: Light") 131 | .font(.title3) 132 | .fontWeight(.light) 133 | 134 | Text("Hello, world: Thin") 135 | .font(.title3) 136 | .fontWeight(.thin) 137 | 138 | Text("Hello, world: Ultra Light") 139 | .font(.title3) 140 | .fontWeight(.ultraLight) 141 | """ 142 | } 143 | 144 | Text("Hello, world: Black") 145 | .font(.title3) 146 | .fontWeight(.black) 147 | 148 | Text("Hello, world: Heavy") 149 | .font(.title3) 150 | .fontWeight(.heavy) 151 | 152 | Text("Hello, world: Bold") 153 | .font(.title3) 154 | .fontWeight(.bold) 155 | 156 | Text("Hello, world: Semibold") 157 | .font(.title3) 158 | .fontWeight(.semibold) 159 | 160 | Text("Hello, world: Medium") 161 | .font(.title3) 162 | .fontWeight(.medium) 163 | 164 | Text("Hello, world: Regular") 165 | .font(.title3) 166 | .fontWeight(.regular) 167 | 168 | Text("Hello, world: Light") 169 | .font(.title3) 170 | .fontWeight(.light) 171 | 172 | Text("Hello, world: Thin") 173 | .font(.title3) 174 | .fontWeight(.thin) 175 | 176 | Text("Hello, world: Ultra Light") 177 | .font(.title3) 178 | .fontWeight(.ultraLight) 179 | .margin(.bottom, .xLarge) 180 | 181 | Text(markdown: "Use the `horizontalAlignment()` modifier to adjust how your text is aligned:") 182 | 183 | CodeBlock(.swift) { 184 | """ 185 | Text("This is left-aligned text.") 186 | .horizontalAlignment(.leading) 187 | 188 | Text("This is center-aligned text.") 189 | .horizontalAlignment(.center) 190 | 191 | Text("This is right-aligned text.") 192 | .horizontalAlignment(.trailing) 193 | """ 194 | } 195 | 196 | Text("This is left-aligned text.") 197 | .horizontalAlignment(.leading) 198 | 199 | Text("This is center-aligned text.") 200 | .horizontalAlignment(.center) 201 | 202 | Text("This is right-aligned text.") 203 | .horizontalAlignment(.trailing) 204 | .margin(.bottom, .xLarge) 205 | 206 | Text(markdown: "Change your text's color using the `foregroundStyle()` modifier. This can be one of the standard roles, e.g. `.danger`, or can be a regular color.") 207 | 208 | CodeBlock(.swift) { 209 | """ 210 | Text("This is dangerous text.") 211 | .foregroundStyle(.danger) 212 | 213 | Text("This is steel blue text.") 214 | .foregroundStyle(.steelBlue) 215 | """ 216 | } 217 | 218 | Text("This is dangerous text.") 219 | .foregroundStyle(.danger) 220 | 221 | Text("This is steel blue text.") 222 | .foregroundStyle(.steelBlue) 223 | .margin(.bottom, .xLarge) 224 | 225 | Text(markdown: "Limit the number of lines your text displays using the `lineLimit()` modifier:") 226 | 227 | CodeBlock(.swift) { 228 | """ 229 | Text(placeholderLength: 200) 230 | .lineLimit(2) 231 | """ 232 | } 233 | 234 | Text(placeholderLength: 200) 235 | .lineLimit(2) 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /Sources/Pages/Concepts/ThemeExamples.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThemeExamples.swift 3 | // IgniteSamples 4 | // https://www.github.com/twostraws/Ignite 5 | // See LICENSE for license information. 6 | // 7 | 8 | import Foundation 9 | import Ignite 10 | 11 | struct ThemeExamples: StaticPage { 12 | var title = "Themes" 13 | var description = """ 14 | Customize your site's appearance with light, dark, and custom themes. \ 15 | Learn theme composition to share styles between themes, create a \ 16 | theme switcher, and use Material to create beautiful blur effects. 17 | """ 18 | 19 | @Environment(\.themes) private var themes 20 | 21 | private var themeSwitcher: some HTML { 22 | VStack(alignment: .center) { 23 | Section { 24 | ForEach(themes) { theme in 25 | Button(theme.name.capitalized) { 26 | SwitchTheme(theme) 27 | } 28 | .role(.light) 29 | .margin(.horizontal, .xSmall) 30 | } 31 | } 32 | .padding(.vertical, .small) 33 | } 34 | .position(.fixedBottom) 35 | .background(.firebrick) 36 | .frame(maxWidth: .percent(100%)) 37 | } 38 | 39 | var body: some HTML { 40 | Text("Themes") 41 | .font(.title1) 42 | 43 | Text("Themes let you customize the appearance of your site, with built-in support for light and dark modes.") 44 | .font(.lead) 45 | 46 | Text("Creating themes") 47 | .font(.title2) 48 | .margin(.top, .xLarge) 49 | 50 | Text(markdown: """ 51 | Themes conform to the `Theme` protocol, which provides sensible defaults for both \ 52 | light and dark appearances. When creating a theme, you specify its `colorScheme` to \ 53 | determine whether it uses light or dark variants of Ignite's default styles. 54 | """) 55 | 56 | CodeBlock(.swift) { 57 | """ 58 | struct MyTheme: Theme { 59 | var colorScheme: ColorScheme = .light // or .dark 60 | 61 | // Override any theme properties you want to customize 62 | var syntaxHighlighterTheme: HighlighterTheme = .githubLight 63 | } 64 | """ 65 | } 66 | 67 | Text("Theme composition") 68 | .font(.title2) 69 | .margin(.top, .xLarge) 70 | 71 | Text(markdown: """ 72 | You can create a base theme protocol that provides common values for multiple themes. This allows you to share styling \ 73 | between themes while still having color scheme specific values for properties you haven't explicitly set. 74 | """) 75 | 76 | CodeBlock(.swift) { 77 | """ 78 | // Base protocol with shared theme values 79 | protocol BaseTheme: Theme {} 80 | 81 | // Default implementation for shared values 82 | extension BaseTheme { 83 | var accent: Color { Color(hex: "#FF0000") } 84 | var secondaryAccent: Color { Color(hex: "#00FF00") } 85 | } 86 | 87 | // Light theme implementation 88 | struct LightTheme: BaseTheme { 89 | var colorScheme: ColorScheme = .light 90 | // Uses shared accent/secondaryAccent colors 91 | // Other values default to stock light theme colors 92 | } 93 | 94 | // Dark theme implementation 95 | struct DarkTheme: BaseTheme { 96 | var colorScheme: ColorScheme = .dark 97 | // Uses shared accent/secondaryAccent colors 98 | // Other values default to stock dark theme colors 99 | } 100 | """ 101 | } 102 | 103 | Text(markdown: "To enable themes in your site, add them to your site configuration:") 104 | 105 | CodeBlock(.swift) { 106 | """ 107 | struct ExampleSite: Site { 108 | var lightTheme: (any Theme)? = LightTheme() 109 | var darkTheme: (any Theme)? = DarkTheme() 110 | var alternateThemes: [any Theme] = [ 111 | SeaTheme(), 112 | DesertTheme(), 113 | ForestTheme() 114 | ] 115 | } 116 | """ 117 | } 118 | 119 | Alert { 120 | Text(markdown: "**Note:** You can add additional themes through `Site`'s `alternateThemes` property.") 121 | } 122 | .role(.info) 123 | 124 | Text(markdown: "You can disable either light or dark mode by setting the corresponding theme to `nil`:") 125 | 126 | CodeBlock(.swift) { 127 | """ 128 | struct DarkOnlySite: Site { 129 | var lightTheme: (any Theme)? = nil // Disable light mode 130 | var darkTheme: (any Theme)? = DarkTheme() 131 | } 132 | 133 | struct LightOnlySite: Site { 134 | var lightTheme: (any Theme)? = LightTheme() 135 | var darkTheme: (any Theme)? = nil // Disable dark mode 136 | } 137 | """ 138 | } 139 | 140 | Alert { 141 | Text(markdown: """ 142 | **Note:** You must provide at least one theme (either light or dark). Setting both `lightTheme` \ 143 | and `darkTheme` to `nil` will result in a build error. 144 | """) 145 | } 146 | .role(.info) 147 | 148 | Text("Theme switching") 149 | .font(.title2) 150 | .margin(.top, .xLarge) 151 | 152 | Text(markdown: """ 153 | You can access all available themes through the environment, making it easy to create a theme switcher. \ 154 | The `SwitchTheme()` action lets users change themes dynamically. 155 | """) 156 | 157 | CodeBlock(.swift) { 158 | """ 159 | struct ThemeSwitcher: HTML { 160 | @Environment(\\.themes) private var themes 161 | 162 | var body: some HTML { 163 | ForEach(themes) { theme in 164 | Button(theme.name) { 165 | SwitchTheme(theme) 166 | } 167 | } 168 | } 169 | } 170 | """ 171 | } 172 | 173 | Text("Give the theme switcher at the bottom of this page a whirl to see this all in action. 👇") 174 | 175 | themeSwitcher 176 | 177 | Text("Materials") 178 | .font(.title2) 179 | .margin(.top, .xLarge) 180 | 181 | Text(markdown: """ 182 | When using `Material`, the correct variant (light or dark) will automatically be selected \ 183 | based on your theme's `colorScheme` value. 184 | """) 185 | 186 | Text(markdown: """ 187 | You can override the default `Material` variant using the `colorScheme()` method, \ 188 | which is useful when you want components to always use a specific appearance: 189 | """) 190 | 191 | CodeBlock(.swift) { 192 | """ 193 | ZStack { 194 | Image("/images/photos/dishwasher.jpg") 195 | .resizable() 196 | Text("Always Dark Material") 197 | .font(.title3) 198 | .containerRelativeFrame() 199 | .background(.thinMaterial.colorScheme(.dark)) 200 | } 201 | .clipped() 202 | .cornerRadius(12) 203 | """ 204 | } 205 | 206 | Text(markdown: "Here's an example using `Material`:") 207 | 208 | Grid { 209 | ZStack { 210 | Image("/images/photos/dishwasher.jpg") 211 | .resizable() 212 | Text("Adaptive Material") 213 | .font(.title3) 214 | .containerRelativeFrame() 215 | .background(.thinMaterial) 216 | .margin(.bottom, 0) 217 | } 218 | .clipped() 219 | .cornerRadius(12) 220 | 221 | ZStack { 222 | Image("/images/photos/dishwasher.jpg") 223 | .resizable() 224 | Text("Always Dark Material") 225 | .font(.title3) 226 | .containerRelativeFrame() 227 | .background(.thinMaterial.colorScheme(.dark)) 228 | .margin(.bottom, 0) 229 | } 230 | .clipped() 231 | .cornerRadius(12) 232 | } 233 | .margin(.vertical, .large) 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /Sources/Pages/Examples/CardExamples.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardExamples.swift 3 | // IgniteSamples 4 | // https://www.github.com/twostraws/Ignite 5 | // See LICENSE for license information. 6 | // 7 | 8 | import Foundation 9 | import Ignite 10 | 11 | struct CardExamples: StaticPage { 12 | var title = "Cards" 13 | var description = """ 14 | Create flexible content containers with images, headers, and footers. \ 15 | Design cards with different styles, roles, and content layouts, including \ 16 | overlay text positioning and image opacity controls. Supports default, \ 17 | solid, and bordered variants with semantic styling. 18 | """ 19 | 20 | var body: some HTML { 21 | Text("Cards") 22 | .font(.title1) 23 | 24 | Text("Cards provide a standard way of representing group information.") 25 | .font(.lead) 26 | 27 | Text("They have various options depending on what data you want to provide and how you want it to look.") 28 | 29 | Text("Here's an example card, with image, content, and a button going back to the homepage:") 30 | 31 | CodeBlock(.swift) { 32 | """ 33 | Card(imageName: "/images/photos/dishwasher.jpg") { 34 | Text("Before putting your dishes into the dishwasher, make sure and give them a quick pre-clean.") 35 | 36 | Link("Back to the homepage", target: "/") 37 | .linkStyle(.button) 38 | } 39 | .frame(maxWidth: 500) 40 | """ 41 | } 42 | 43 | Card(imageName: "/images/photos/dishwasher.jpg") { 44 | Text("Before putting your dishes into the dishwasher, make sure and give them a quick pre-clean.") 45 | 46 | Link("Back to the homepage", target: "/") 47 | .linkStyle(.button) 48 | } 49 | .frame(maxWidth: 500) 50 | 51 | Text(markdown: "It's also possible to omit the `imageName` parameter and simply place a image in the content:") 52 | .margin(.top, .xLarge) 53 | 54 | CodeBlock(.swift) { 55 | """ 56 | Card { 57 | Text("An image embedded") 58 | Image(decorative: "/images/photos/rug.jpg") 59 | Text("as part of the card") 60 | } 61 | .frame(maxWidth: 500) 62 | """ 63 | } 64 | 65 | Card { 66 | Text("An image embedded") 67 | Image(decorative: "/images/photos/rug.jpg") 68 | Text("as part of the card") 69 | } 70 | .frame(maxWidth: 500) 71 | 72 | Text("Content position") 73 | .font(.lead) 74 | 75 | Text(markdown: """ 76 | By default the contents of your card are positioned below any image, but you can change that using 77 | `.contentPosition()` with a value of `ContentPosition.top` or `ContentPosition.bottom`. 78 | """) 79 | 80 | CodeBlock(.swift) { 81 | """ 82 | Card(imageName: "/images/photos/rug.jpg") { 83 | Text(markdown: "Content below image: `.top`") 84 | } 85 | .contentPosition(.top) 86 | """ 87 | } 88 | 89 | Grid { 90 | Card(imageName: "/images/photos/rug.jpg") { 91 | Text(markdown: "Content below image: use `.top`") 92 | } 93 | .contentPosition(.top) 94 | 95 | Card(imageName: "/images/photos/rug.jpg") { 96 | Text(markdown: "Content below image: use `.bottom`") 97 | } 98 | .contentPosition(.bottom) 99 | } 100 | .columns(2) 101 | 102 | Text("Overlaying text") 103 | .font(.title2) 104 | .margin(.top, .xLarge) 105 | 106 | Text(markdown: """ 107 | By default the contents of your card are positioned below any image, but you can change that using \ 108 | `.contentPosition(.overlay)` and optionally also the `imageOpacity()` modifier. 109 | """) 110 | 111 | CodeBlock(.swift) { 112 | """ 113 | Card(imageName: "/images/photos/dishwasher.jpg") { 114 | Text("Before putting your dishes into the dishwasher, make sure and give them a quick pre-clean.") 115 | .foregroundStyle(.white) 116 | 117 | Link("Back to the homepage", target: "/") 118 | .linkStyle(.button) 119 | } 120 | .frame(maxWidth: 500) 121 | .contentPosition(.overlay) 122 | .imageOpacity(0.5) 123 | """ 124 | } 125 | 126 | Card(imageName: "/images/photos/dishwasher.jpg") { 127 | Text("Before putting your dishes into the dishwasher, make sure and give them a quick pre-clean.") 128 | .foregroundStyle(.white) 129 | 130 | Link("Back to the homepage", target: "/") 131 | .linkStyle(.button) 132 | } 133 | .contentPosition(.overlay) 134 | .imageOpacity(0.5) 135 | .frame(maxWidth: 500) 136 | 137 | Text(markdown: """ 138 | To control the position of the overlay you can specify an alignment using `.overlay(alignment:)` 139 | with one of the following options: 140 | """) 141 | .margin(.top, .large) 142 | 143 | Grid(Card.ContentAlignment.allCases) { alignment in 144 | Card(imageName: "/images/photos/dishwasher.jpg") { 145 | let alignmentName = String(describing: alignment) 146 | Text(markdown: "`.\(alignmentName)`") 147 | .foregroundStyle(.white) 148 | .background(.lightGray) 149 | 150 | Link("Back to the homepage", target: "/") 151 | .linkStyle(.button) 152 | } 153 | .contentPosition(.overlay(alignment: alignment)) 154 | .imageOpacity(0.5) 155 | } 156 | .columns(3) 157 | .margin(.top, .large) 158 | 159 | Text("Headers and footers") 160 | .font(.title2) 161 | .margin(.top, .xLarge) 162 | 163 | Text(markdown: """ 164 | You can attach headers and/or footers to your cards, and they automatically get styled appropriately. \ 165 | This is in *addition* to any image you provide, or any titles used in the card body. 166 | """) 167 | 168 | CodeBlock(.swift) { 169 | """ 170 | Card { 171 | Text("This is important!") 172 | .font(.title3) 173 | Text("This is card body text. This is card body text. This is card body text. This is card body text. This is card body text.") 174 | } header: { 175 | "Header Example" 176 | } footer: { 177 | "Your footer goes here footer" 178 | } 179 | """ 180 | } 181 | 182 | Card { 183 | Text("This is important!") 184 | .font(.title3) 185 | Text("This is card body text. This is card body text. This is card body text. This is card body text. This is card body text.") 186 | } header: { 187 | "Header Example" 188 | } footer: { 189 | "Your footer goes here footer" 190 | } 191 | 192 | Text("Card styles") 193 | .font(.title2) 194 | .margin(.top, .xLarge) 195 | 196 | Text(markdown: """ 197 | Cards have three styles: `.default`, `.solid`, and `.bordered`, and can also be assigned roles. \ 198 | The `.default` style does nothing special, but both `.solid` and `.bordered` apply coloring based on role. 199 | """) 200 | 201 | CodeBlock(.swift) { 202 | """ 203 | ForEach(Card.Style.allCases) { style in 204 | if style != .default { 205 | Text(markdown: "`\\(style)` style") 206 | .font(.title3) 207 | 208 | ForEach(Role.allCases) { role in 209 | Card { 210 | "This is a \\(style) card with the \\(role) role." 211 | } header: { 212 | "Header" 213 | } 214 | .role(role) 215 | .cardStyle(style) 216 | .margin(.bottom) 217 | } 218 | 219 | Spacer(size: .xLarge) 220 | } 221 | } 222 | """ 223 | } 224 | 225 | ForEach(Card.Style.allCases) { style in 226 | if style != .default { 227 | Text(markdown: "`\(style)` style") 228 | .font(.title3) 229 | .margin(.top, .xLarge) 230 | 231 | ForEach(Role.allCases) { role in 232 | Card { 233 | "This is a \(style) card with the \(role) role." 234 | } header: { 235 | "Header" 236 | } 237 | .role(role) 238 | .cardStyle(style) 239 | .margin(.bottom) 240 | } 241 | } 242 | } 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /Sources/Pages/Examples/TableExamples.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableExamples.swift 3 | // IgniteSamples 4 | // https://www.github.com/twostraws/Ignite 5 | // See LICENSE for license information. 6 | // 7 | 8 | import Foundation 9 | import Ignite 10 | 11 | struct TableExamples: StaticPage { 12 | var title = "Tables" 13 | var description = """ 14 | Create structured data layouts with rows and columns, \ 15 | supporting headers, striped styles, filtering, and custom alignment. \ 16 | Perfect for displaying tabular data clearly. 17 | """ 18 | 19 | var body: some HTML { 20 | Text("Tables") 21 | .font(.title1) 22 | 23 | Text("Tables let you create tabulated data in rows and columns for easier reading.") 24 | .font(.lead) 25 | 26 | Text("Simple tables consist of rows and columns, like this:") 27 | 28 | CodeBlock(.swift) { 29 | """ 30 | Table { 31 | Row { 32 | "Taylor Swift" 33 | "555, Swiftie Avenue" 34 | "Nashville" 35 | "Tennessee" 36 | } 37 | 38 | Row { 39 | "Adele Adkins" 40 | "Caesars Palace" 41 | "Las Vegas" 42 | "Nevada" 43 | } 44 | 45 | Row { 46 | "Tim Cook" 47 | "Apple Park" 48 | "Cupertino" 49 | "California" 50 | } 51 | } 52 | """ 53 | } 54 | 55 | Table { 56 | Row { 57 | "Taylor Swift" 58 | "555, Swiftie Avenue" 59 | "Nashville" 60 | "Tennessee" 61 | } 62 | 63 | Row { 64 | "Adele Adkins" 65 | "Caesars Palace" 66 | "Las Vegas" 67 | "Nevada" 68 | } 69 | 70 | Row { 71 | "Tim Cook" 72 | "Apple Park" 73 | "Cupertino" 74 | "California" 75 | } 76 | } 77 | .margin(.bottom, .xLarge) 78 | 79 | Text("In that example, all rows are fixed directly as Swift code. If you prefer, you can pass in an array:") 80 | 81 | CodeBlock(.swift) { 82 | """ 83 | Table(User.examples) { user in 84 | Row { 85 | user.name 86 | user.address 87 | user.city 88 | user.state 89 | } 90 | } 91 | .margin(.bottom, .xLarge) 92 | """ 93 | } 94 | 95 | Table(User.examples) { user in 96 | Row { 97 | user.name 98 | user.address 99 | user.city 100 | user.state 101 | } 102 | } 103 | .margin(.bottom, .xLarge) 104 | 105 | Text("You can also add headings to clarify what each column means:") 106 | 107 | CodeBlock(.swift) { 108 | """ 109 | Table(User.examples) { user in 110 | Row { 111 | user.name 112 | user.address 113 | user.city 114 | user.state 115 | } 116 | } header: { 117 | "Name" 118 | "Address" 119 | "City" 120 | "State" 121 | } 122 | """ 123 | } 124 | 125 | Table(User.examples) { user in 126 | Row { 127 | user.name 128 | user.address 129 | user.city 130 | user.state 131 | } 132 | } header: { 133 | "Name" 134 | "Address" 135 | "City" 136 | "State" 137 | } 138 | .margin(.bottom, .xLarge) 139 | 140 | Text("Table styling") 141 | .font(.title2) 142 | 143 | Text(markdown: "To make rows easier to distinguish, use `.tableStyle(.stripedRows)` to create a zebra striping effect:") 144 | 145 | CodeBlock(.swift) { 146 | """ 147 | Table(TeamMember.examples) { teamMember in 148 | Row { 149 | teamMember.name 150 | teamMember.occupation 151 | teamMember.favoriteFood 152 | teamMember.secretHobby 153 | } 154 | } header: { 155 | "Name" 156 | "Occupation" 157 | "Favourite Food" 158 | "Secret Hobby" 159 | } 160 | .tableStyle(.stripedRows) 161 | """ 162 | } 163 | 164 | Table(TeamMember.examples) { teamMember in 165 | Row { 166 | teamMember.name 167 | teamMember.occupation 168 | teamMember.favoriteFood 169 | teamMember.secretHobby 170 | } 171 | } header: { 172 | "Name" 173 | "Occupation" 174 | "Favourite Food" 175 | "Secret Hobby" 176 | } 177 | .tableStyle(.stripedRows) 178 | .margin(.bottom, .xLarge) 179 | 180 | Text(markdown: "Alternatively, use `.tableStyle(.stripedColumns)` to create columnar stripes:") 181 | 182 | CodeBlock(.swift) { 183 | """ 184 | Table(TeamMember.examples) { teamMember in 185 | Row { 186 | teamMember.name 187 | teamMember.occupation 188 | teamMember.favoriteFood 189 | teamMember.secretHobby 190 | } 191 | } header: { 192 | "Name" 193 | "Occupation" 194 | "Favourite Food" 195 | "Secret Hobby" 196 | } 197 | .tableStyle(.stripedColumns) 198 | """ 199 | } 200 | 201 | Table(TeamMember.examples) { teamMember in 202 | Row { 203 | teamMember.name 204 | teamMember.occupation 205 | teamMember.favoriteFood 206 | teamMember.secretHobby 207 | } 208 | } header: { 209 | "Name" 210 | "Occupation" 211 | "Favourite Food" 212 | "Secret Hobby" 213 | } 214 | .tableStyle(.stripedColumns) 215 | .margin(.bottom, .xLarge) 216 | 217 | Text("Filtering tables") 218 | .font(.title2) 219 | 220 | Text(markdown: "If you want users to be able to filter the data in your table, add a `filterTitle` parameter when you create it. This will filter on any text in each row.") 221 | 222 | CodeBlock(.swift) { 223 | """ 224 | Table(Customer.examples, filterTitle: "Search for a user") { customer in 225 | Row { 226 | customer.name 227 | customer.city 228 | customer.country 229 | } 230 | } 231 | .tableStyle(.stripedRows) 232 | """ 233 | } 234 | 235 | Table(Customer.examples, filterTitle: "Search for a user") { customer in 236 | Row { 237 | customer.name 238 | customer.city 239 | customer.country 240 | } 241 | } 242 | .tableStyle(.stripedRows) 243 | .margin(.bottom, .xLarge) 244 | 245 | Text("Row and column formatting") 246 | .font(.title2) 247 | 248 | Text(markdown: """ 249 | Rows are top-aligned by default, but you can change that by adding a `Column` then using its `verticalAlignment()` modifier. 250 | This is easier to see when the table borders is enabled using the `tableBorder()` modifier, like this: 251 | """ 252 | ) 253 | 254 | CodeBlock(.swift) { 255 | """ 256 | Table { 257 | Row { 258 | Column { 259 | "Top" 260 | } 261 | .verticalAlignment(.top) 262 | 263 | Column { 264 | "Middle" 265 | } 266 | .verticalAlignment(.middle) 267 | 268 | Column { 269 | "Bottom" 270 | } 271 | .verticalAlignment(.bottom) 272 | 273 | Column { 274 | \""" 275 | This is much longer text and will wrap over multiple lines, so we can see vertical alignment working. \ 276 | This is much longer text and will wrap over multiple lines, so we can see vertical alignment working. \ 277 | This is much longer text and will wrap over multiple lines, so we can see vertical alignment working. \ 278 | This is much longer text and will wrap over multiple lines, so we can see vertical alignment working. \ 279 | This is much longer text and will wrap over multiple lines, so we can see vertical alignment working. 280 | \""" 281 | } 282 | } 283 | } 284 | .tableBorder(true) 285 | """ 286 | } 287 | 288 | Table { 289 | Row { 290 | Column { 291 | "Top" 292 | } 293 | .verticalAlignment(.top) 294 | 295 | Column { 296 | "Middle" 297 | } 298 | .verticalAlignment(.middle) 299 | 300 | Column { 301 | "Bottom" 302 | } 303 | .verticalAlignment(.bottom) 304 | 305 | Column { 306 | """ 307 | This is much longer text and will wrap over multiple lines, so we can see vertical alignment working. \ 308 | This is much longer text and will wrap over multiple lines, so we can see vertical alignment working. \ 309 | This is much longer text and will wrap over multiple lines, so we can see vertical alignment working. \ 310 | This is much longer text and will wrap over multiple lines, so we can see vertical alignment working. \ 311 | This is much longer text and will wrap over multiple lines, so we can see vertical alignment working. 312 | """ 313 | } 314 | } 315 | } 316 | .tableBorder(true) 317 | .margin(.bottom, .xLarge) 318 | 319 | Text(markdown: "Each row item occupies one column by default, but `Column` objects have a `columnSpan()` modifier to adjust that:") 320 | 321 | CodeBlock(.swift) { 322 | """ 323 | Table { 324 | Row { 325 | "This is a column" 326 | "Another column" 327 | "One more for luck" 328 | } 329 | 330 | Row { 331 | Column { 332 | "This column is as wide as the previous three, and is aligned to the center." 333 | } 334 | .columnSpan(3) 335 | .horizontalAlignment(.center) 336 | } 337 | 338 | Row { 339 | "This is another column" 340 | 341 | Column { 342 | "This column occupies two slots, and is aligned to the trailing edge." 343 | } 344 | .columnSpan(2) 345 | .horizontalAlignment(.trailing) 346 | } 347 | } 348 | .tableBorder(true) 349 | .tableStyle(.stripedRows) 350 | """ 351 | } 352 | 353 | Table { 354 | Row { 355 | "This is a column" 356 | "Another column" 357 | "One more for luck" 358 | } 359 | 360 | Row { 361 | Column { 362 | "This column is as wide as the previous three, and is aligned to the center." 363 | } 364 | .columnSpan(3) 365 | .horizontalAlignment(.center) 366 | } 367 | 368 | Row { 369 | "This is another column" 370 | 371 | Column { 372 | "This column occupies two slots, and is aligned to the trailing edge." 373 | } 374 | .columnSpan(2) 375 | .horizontalAlignment(.trailing) 376 | } 377 | } 378 | .tableBorder(true) 379 | .tableStyle(.stripedRows) 380 | } 381 | } 382 | -------------------------------------------------------------------------------- /Sources/Pages/Examples/ModalExamples.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModalExamples.swift 3 | // IgniteSamples 4 | // https://www.github.com/twostraws/Ignite 5 | // See LICENSE for license information. 6 | // 7 | 8 | import Foundation 9 | import Ignite 10 | 11 | struct ModalExamples: StaticPage { 12 | var title = "Modals" 13 | var description = """ 14 | Learn how to create and customize modals with different sizes, \ 15 | positions, headers, and footers. See how to make content scrollable \ 16 | and control modal behavior with presentation options. 17 | """ 18 | 19 | var body: some HTML { 20 | Text("Modals") 21 | .font(.title1) 22 | 23 | Text("Modals allow you to present content in a modal dialog on top of the current page. By default they are presented with a backdrop, vertically centered in a medium size on the screen.") 24 | .font(.lead) 25 | 26 | // MARK: - Showing Modals 27 | 28 | Text("Showing Modals") 29 | .font(.title3) 30 | 31 | Text(markdown: "A modal is displayed using the `ShowModal(id:)` action.") 32 | 33 | Text(markdown: "Here is simple modal:") 34 | .margin(.top, .large) 35 | 36 | let showModalId = "showModalId" 37 | 38 | CodeBlock(.swift) { 39 | """ 40 | Button("Show simple Modal") { 41 | ShowModal(id: "\(showModalId)") 42 | } 43 | .role(.primary) 44 | 45 | Modal(id: "\(showModalId)") { 46 | Text("Dismiss me by clicking on the backdrop.") 47 | .horizontalAlignment(.center) 48 | .font(.title3) 49 | .margin(.xLarge) 50 | } 51 | """ 52 | } 53 | 54 | Section { 55 | Card { 56 | Section { 57 | Button("Show simple Modal") { 58 | ShowModal(id: showModalId) 59 | } 60 | .role(.primary) 61 | } 62 | .margin(.vertical, .medium) 63 | } 64 | .role(.light) 65 | 66 | Modal(id: showModalId) { 67 | Text("Dismiss me by clicking on the backdrop.") 68 | .horizontalAlignment(.center) 69 | .font(.title3) 70 | .margin(.xLarge) 71 | } 72 | } 73 | .padding(.top, .large) 74 | 75 | Text("Dismissing Modals") 76 | .font(.title2) 77 | .margin(.top, .xLarge) 78 | 79 | Text(markdown: "Modals can either be dismissed by clicking on the backdrop, unless otherwise configured in the [Presentation Options](#Options), by pressing the `Esc` key or programmatically by using the `DismissModal(id:)` action.") 80 | 81 | Text(markdown: "This modal can be dismissed through a cose button in the top right corner:") 82 | .margin(.top, .medium) 83 | 84 | let dismissModalId = "dismissModalId" 85 | 86 | CodeBlock(.swift) { 87 | """ 88 | Modal(id: "\(dismissModalId)") { 89 | Section { 90 | Button().role(.close).onClick { 91 | DismissModal(id: "\(dismissModalId)") 92 | } 93 | } 94 | .horizontalAlignment(.trailing) 95 | 96 | Text("Dismiss me by clicking on the close button.") 97 | .horizontalAlignment(.center) 98 | .font(.title3) 99 | .margin(.xLarge) 100 | } 101 | """ 102 | } 103 | 104 | Section { 105 | Card { 106 | Section { 107 | Button("Show Modal with a Close Button") { 108 | ShowModal(id: dismissModalId) 109 | } 110 | .role(.primary) 111 | } 112 | .margin(.vertical, .medium) 113 | } 114 | .role(.light) 115 | 116 | Modal(id: dismissModalId) { 117 | Section { 118 | Button().role(.close).onClick { 119 | DismissModal(id: dismissModalId) 120 | } 121 | } 122 | .horizontalAlignment(.trailing) 123 | 124 | Text("Dismiss me by clicking on the close button.") 125 | .horizontalAlignment(.center) 126 | .font(.title3) 127 | .margin(.xLarge) 128 | } 129 | } 130 | .padding(.top, .large) 131 | 132 | // MARK: - Modal Size 133 | 134 | Text("Modal Size") 135 | .font(.title2) 136 | .margin(.top, .xLarge) 137 | 138 | Text(markdown: "The height of a modal is determined by the height of the content. However, the width can be adjusted by using the `size()` modifier. By default a modal takes a medium width but it can be adjusted to `.small`, `.large`, `.xLarge` and `.fullscreen`") 139 | 140 | Text(markdown: "Here are a few examples:") 141 | .margin(.top, .medium) 142 | 143 | let smallModalId = "smallModalId" 144 | let xLargeModalId = "xLargeModalId" 145 | let fullscreenModalId = "fullscreenModalId" 146 | 147 | CodeBlock(.swift) { 148 | """ 149 | Modal(id: "\(smallModalId)") { 150 | Text(markdown: "Modal with size `.small`") 151 | .horizontalAlignment(.center) 152 | .font(.title3) 153 | .margin(.xLarge) 154 | } 155 | .size(.small) 156 | 157 | Modal(id: "\(xLargeModalId)") { 158 | Text(markdown: "Modal with size `.xLarge`") 159 | .horizontalAlignment(.center) 160 | .font(.title3) 161 | .margin(.xLarge) 162 | } 163 | .size(.xlarge) 164 | 165 | Modal(id: "\(fullscreenModalId)") { 166 | Section { 167 | Button().role(.close).onClick { 168 | DismissModal(id: "\(fullscreenModalId)") 169 | } 170 | } 171 | .horizontalAlignment(.trailing) 172 | 173 | Text(markdown: "Modal with size `.fullscreen`.") 174 | .horizontalAlignment(.center) 175 | .font(.title3) 176 | .margin(.xLarge) 177 | } 178 | .size(.fullscreen) 179 | """ 180 | } 181 | 182 | Section { 183 | Card { 184 | Section { 185 | Grid { 186 | Section { 187 | Button("Show Small Modal") { 188 | ShowModal(id: smallModalId) 189 | } 190 | .role(.primary) 191 | } 192 | 193 | Section { 194 | Button("Show xLarge Modal") { 195 | ShowModal(id: xLargeModalId) 196 | } 197 | .role(.primary) 198 | } 199 | 200 | Section { 201 | Button("Show Fullscreen Modal") { 202 | ShowModal(id: fullscreenModalId) 203 | } 204 | .role(.primary) 205 | } 206 | } 207 | .horizontalAlignment(.center) 208 | } 209 | .margin(.vertical, .medium) 210 | } 211 | .role(.light) 212 | 213 | Modal(id: "\(smallModalId)") { 214 | Text(markdown: "Modal with size `.small`") 215 | .horizontalAlignment(.center) 216 | .font(.title3) 217 | .margin(.xLarge) 218 | } 219 | .size(.small) 220 | 221 | Modal(id: "\(xLargeModalId)") { 222 | Text(markdown: "Modal with size `.xLarge`") 223 | .horizontalAlignment(.center) 224 | .font(.title3) 225 | .margin(.xLarge) 226 | } 227 | .size(.xLarge) 228 | 229 | Modal(id: "\(fullscreenModalId)") { 230 | Section { 231 | Button().role(.close).onClick { 232 | DismissModal(id: "\(fullscreenModalId)") 233 | } 234 | } 235 | .horizontalAlignment(.trailing) 236 | 237 | Text(markdown: "Modal with size `.fullscreen`.") 238 | .horizontalAlignment(.center) 239 | .font(.title3) 240 | .margin(.xLarge) 241 | } 242 | .size(.fullscreen) 243 | } 244 | .padding(.top, .large) 245 | 246 | // MARK: - Modal Position 247 | 248 | Text("Modal Position") 249 | .font(.title2) 250 | .margin(.top, .xLarge) 251 | 252 | Text(markdown: "Modals are always horizontally centered but the vertical position can be changed from `Position.center` to `Position.top` by using the `.modalPosition()` modifier.") 253 | 254 | Text(markdown: "Here is a modal with `Position.top`:") 255 | .margin(.top, .medium) 256 | 257 | let topModalId = "topModalId" 258 | 259 | CodeBlock(.swift) { 260 | """ 261 | Modal(id: "\(topModalId)") { 262 | Text(markdown: "Modal with `Position.top`") 263 | .horizontalAlignment(.center) 264 | .font(.title3) 265 | .margin(.xLarge) 266 | } 267 | .position(.top) 268 | """ 269 | } 270 | 271 | Section { 272 | Card { 273 | Section { 274 | Button("Show Modal at the top") { 275 | ShowModal(id: topModalId) 276 | } 277 | .role(.primary) 278 | } 279 | .margin(.vertical, .medium) 280 | } 281 | .role(.light) 282 | 283 | Modal(id: topModalId) { 284 | Text(markdown: "Modal with `Position.top`") 285 | .horizontalAlignment(.center) 286 | .font(.title3) 287 | .margin(.xLarge) 288 | } 289 | .modalPosition(.top) 290 | } 291 | .padding(.top, .large) 292 | 293 | Text("Headers and Footers") 294 | .font(.title2) 295 | .margin(.top, .xLarge) 296 | 297 | Text(markdown: "Optionally modals can have a header and/or a footer.") 298 | 299 | Text(markdown: "Here is how they look like:") 300 | .margin(.top, .medium) 301 | 302 | let headerModalId = "headerModalId" 303 | let footerModalId = "footerModalId" 304 | let headerAndFooterModalId = "headerAndFooterModalId" 305 | 306 | CodeBlock(.swift) { 307 | """ 308 | Modal(id: "\(headerModalId)") { 309 | Text("Body") 310 | } header: { 311 | Text("Header").font(.title5) 312 | 313 | Button().role(.close).onClick{ 314 | DismissModal(id: "\(headerModalId)") 315 | } 316 | } 317 | 318 | Modal(id: "\(footerModalId)") { 319 | Text("Body") 320 | } footer: { 321 | Button("Close") { 322 | DismissModal(id: "\(footerModalId)") 323 | } 324 | .role(.secondary) 325 | 326 | Button("Go") { 327 | // Do something 328 | } 329 | .role(.primary) 330 | } 331 | 332 | Modal(id: "\(headerAndFooterModalId)") { 333 | Text("Body") 334 | } header: { 335 | Text("Header").font(.title5) 336 | 337 | Button().role(.close).onClick{ 338 | DismissModal(id: "\(headerAndFooterModalId)") 339 | } 340 | } footer: { 341 | Button("Close") { 342 | DismissModal(id: "\(headerAndFooterModalId)") 343 | } 344 | .role(.secondary) 345 | 346 | Button("Go") { 347 | // Do something 348 | } 349 | .role(.primary) 350 | } 351 | """ 352 | } 353 | 354 | Section { 355 | Card { 356 | Section { 357 | Grid { 358 | Section { 359 | Button("Show Modal with Header") { 360 | ShowModal(id: headerModalId) 361 | } 362 | .role(.primary) 363 | } 364 | 365 | Section { 366 | Button("Show Modal with Footer") { 367 | ShowModal(id: footerModalId) 368 | } 369 | .role(.primary) 370 | } 371 | 372 | Section { 373 | Button("Show Modal with Header and Footer") { 374 | ShowModal(id: headerAndFooterModalId) 375 | } 376 | .role(.primary) 377 | } 378 | } 379 | .horizontalAlignment(.center) 380 | } 381 | .margin(.vertical, .medium) 382 | } 383 | .role(.light) 384 | 385 | Alert { 386 | Text(markdown: "Note how the content in the header is inlined and placed on the leading edge. However, a button with the role `close` is moved to the trailing edge. Content in the footer on the other hand is placed on the trailing edge.") 387 | } 388 | .role(.info) 389 | .margin(.top, .large) 390 | 391 | Modal(id: headerModalId) { 392 | Text("Body") 393 | } header: { 394 | Text("Header").font(.title5) 395 | 396 | Button().role(.close).onClick { 397 | DismissModal(id: headerModalId) 398 | } 399 | } 400 | 401 | Modal(id: footerModalId) { 402 | Text("Body") 403 | } footer: { 404 | Button("Close") { 405 | DismissModal(id: footerModalId) 406 | } 407 | .role(.secondary) 408 | 409 | Button("Go") { 410 | // Do something 411 | } 412 | .role(.primary) 413 | } 414 | 415 | Modal(id: headerAndFooterModalId) { 416 | Text("Body") 417 | } header: { 418 | Text("Header").font(.title5) 419 | 420 | Button().role(.close).onClick { 421 | DismissModal(id: headerAndFooterModalId) 422 | } 423 | } footer: { 424 | Button("Close") { 425 | DismissModal(id: headerAndFooterModalId) 426 | } 427 | .role(.secondary) 428 | 429 | Button("Go") { 430 | // Do something 431 | } 432 | .role(.primary) 433 | } 434 | } 435 | .padding(.top, .large) 436 | 437 | Text("Scrollable Content") 438 | .font(.title2) 439 | .margin(.top, .xLarge) 440 | 441 | Text(markdown: "For content that is too long to fit on the screen, the body content can be made scrollable by using the `scrollableContent()` modifier.") 442 | 443 | Text(markdown: "Here is a Modal with a long text:") 444 | .margin(.top, .medium) 445 | 446 | let modal7 = "modal7" 447 | 448 | CodeBlock(.swift) { 449 | """ 450 | Modal(id: "\(modal7)" { 451 | Text(placeholderLength: 1000) 452 | } header: { 453 | Text("Long text") 454 | .font(.title5) 455 | } 456 | .size(.large) 457 | .scrollableContent(true) 458 | """ 459 | } 460 | 461 | Section { 462 | Card { 463 | Section { 464 | Button("Show Modal with Scrollable Content") { 465 | ShowModal(id: modal7) 466 | } 467 | .role(.primary) 468 | } 469 | .margin(.vertical, .medium) 470 | } 471 | .role(.light) 472 | 473 | Modal(id: modal7) { 474 | Text(placeholderLength: 1000) 475 | } header: { 476 | Text("Long text") 477 | .font(.title5) 478 | } 479 | .size(.large) 480 | .scrollableContent(true) 481 | } 482 | 483 | Text("Presentation Options") 484 | .font(.title2) 485 | .margin(.top, .xLarge) 486 | .id("Options") 487 | 488 | Text(markdown: "The `ShowModal` action accepts an array of options that allow you to configure various presentation settings:") 489 | 490 | Table { 491 | Row { 492 | Text("Option").font(.title5) 493 | Text("Description").font(.title5) 494 | } 495 | Row { 496 | Text(markdown: "`noBackdrop`") 497 | Text(markdown: "Disables the backdrop, making the modal non-dismissible by clicking outside it.") 498 | } 499 | Row { 500 | Text(markdown: "`backdrop(dismissible: Bool)`") 501 | Text(markdown: "Enables the backdrop by default. If `dismissible` is set to `false`, the modal cannot be dismissed by clicking outside.") 502 | } 503 | Row { 504 | Text(markdown: "`keyboard(Bool)`") 505 | Text(markdown: "Allows the modal to be dismissed by pressing the ESC key when set to `true`. If `false`, this behavior is disabled. The default is `true`.") 506 | } 507 | Row { 508 | Text(markdown: "`focus(Bool)`") 509 | Text(markdown: "When `true`, the modal will autofocus on the modal when opened. The default is `true`.") 510 | } 511 | } 512 | 513 | Text(markdown: "A modal which can not be dismissed by clicking on the backdrop can be configured like this:") 514 | .margin(.top, .xLarge) 515 | 516 | CodeBlock(.swift) { 517 | """ 518 | Button("Show Modal") { 519 | ShowModal(id: "showModalId", option [.backdrop(dismissable: false)]) 520 | } 521 | """ 522 | } 523 | } 524 | } 525 | --------------------------------------------------------------------------------