diff --git a/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/IntroducingSlipstream.tutorial b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/IntroducingSlipstream.tutorial index 8a18a88e..c738f5cb 100644 --- a/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/IntroducingSlipstream.tutorial +++ b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/IntroducingSlipstream.tutorial @@ -7,7 +7,7 @@ @Chapter(name: "Your first site") { Learn the basics of making a static website with Slipstream. - + And there's cute pigs too 🐷 @Image(source: "IntroducingSlipstream-YourFirstSite", alt: "The Slipstream logo. The Swift bird logo is flying off the edge of the Tailwind CSS wind logo") @@ -15,5 +15,8 @@ @TutorialReference(tutorial: "doc:IntroducingSlipstream-YourFirstSite-Workspace") @TutorialReference(tutorial: "doc:IntroducingSlipstream-YourFirstSite-HelloWorld") @TutorialReference(tutorial: "doc:IntroducingSlipstream-YourFirstSite-TextImageLink") + @TutorialReference(tutorial: "doc:IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive") + @TutorialReference(tutorial: "doc:IntroducingSlipstream-YourFirstSite-MultiPageSite") + @TutorialReference(tutorial: "doc:IntroducingSlipstream-YourFirstSite-FormsAndInput") } } diff --git a/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput-1-1.swift b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput-1-1.swift new file mode 100644 index 00000000..8d4c0d8f --- /dev/null +++ b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput-1-1.swift @@ -0,0 +1,40 @@ +import Foundation + +import Slipstream + +struct Contact: View { + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + Container { + H1("Contact Coco") + .fontSize(.extraExtraExtraLarge) + .bold() + .margin(.bottom, 32) + + Text("Want to say hello to Coco? Fill out the form below!") + .margin(.bottom, 24) + } + .padding(.vertical, 48) + } + } + } +} + +let sitemap: Sitemap = [ + "contact.html": Contact() +] + +guard let projectURL = URL(filePath: #filePath)? + .deletingLastPathComponent() + .deletingLastPathComponent() else { + print("Unable to create URL for \(#filePath)") + exit(1) +} + +let outputURL = projectURL.appending(path: "site") + +try renderSitemap(sitemap, to: outputURL) diff --git a/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput-1-2.swift b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput-1-2.swift new file mode 100644 index 00000000..ac686296 --- /dev/null +++ b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput-1-2.swift @@ -0,0 +1,44 @@ +import Foundation + +import Slipstream + +struct Contact: View { + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + Container { + H1("Contact Coco") + .fontSize(.extraExtraExtraLarge) + .bold() + .margin(.bottom, 32) + + Text("Want to say hello to Coco? Fill out the form below!") + .margin(.bottom, 24) + + Form(method: .post, url: URL(string: "/submit")) { + // Form fields will go here + } + } + .padding(.vertical, 48) + } + } + } +} + +let sitemap: Sitemap = [ + "contact.html": Contact() +] + +guard let projectURL = URL(filePath: #filePath)? + .deletingLastPathComponent() + .deletingLastPathComponent() else { + print("Unable to create URL for \(#filePath)") + exit(1) +} + +let outputURL = projectURL.appending(path: "site") + +try renderSitemap(sitemap, to: outputURL) diff --git a/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput-1-3.swift b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput-1-3.swift new file mode 100644 index 00000000..ac75b719 --- /dev/null +++ b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput-1-3.swift @@ -0,0 +1,45 @@ +import Foundation + +import Slipstream + +struct Contact: View { + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + Container { + H1("Contact Coco") + .fontSize(.extraExtraExtraLarge) + .bold() + .margin(.bottom, 32) + + Text("Want to say hello to Coco? Fill out the form below!") + .margin(.bottom, 24) + + Form(method: .post, url: URL(string: "/submit")) { + TextField("Your name", type: .text, name: "name") + TextField("Your email", type: .email, name: "email") + } + } + .padding(.vertical, 48) + } + } + } +} + +let sitemap: Sitemap = [ + "contact.html": Contact() +] + +guard let projectURL = URL(filePath: #filePath)? + .deletingLastPathComponent() + .deletingLastPathComponent() else { + print("Unable to create URL for \(#filePath)") + exit(1) +} + +let outputURL = projectURL.appending(path: "site") + +try renderSitemap(sitemap, to: outputURL) diff --git a/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput-1-4.swift b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput-1-4.swift new file mode 100644 index 00000000..fddf140b --- /dev/null +++ b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput-1-4.swift @@ -0,0 +1,46 @@ +import Foundation + +import Slipstream + +struct Contact: View { + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + Container { + H1("Contact Coco") + .fontSize(.extraExtraExtraLarge) + .bold() + .margin(.bottom, 32) + + Text("Want to say hello to Coco? Fill out the form below!") + .margin(.bottom, 24) + + Form(method: .post, url: URL(string: "/submit")) { + TextField("Your name", type: .text, name: "name") + TextField("Your email", type: .email, name: "email") + TextArea("Your message", name: "message", rows: 5) + } + } + .padding(.vertical, 48) + } + } + } +} + +let sitemap: Sitemap = [ + "contact.html": Contact() +] + +guard let projectURL = URL(filePath: #filePath)? + .deletingLastPathComponent() + .deletingLastPathComponent() else { + print("Unable to create URL for \(#filePath)") + exit(1) +} + +let outputURL = projectURL.appending(path: "site") + +try renderSitemap(sitemap, to: outputURL) diff --git a/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput-1-5.swift b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput-1-5.swift new file mode 100644 index 00000000..e2a62f9f --- /dev/null +++ b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput-1-5.swift @@ -0,0 +1,49 @@ +import Foundation + +import Slipstream + +struct Contact: View { + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + Container { + H1("Contact Coco") + .fontSize(.extraExtraExtraLarge) + .bold() + .margin(.bottom, 32) + + Text("Want to say hello to Coco? Fill out the form below!") + .margin(.bottom, 24) + + Form(method: .post, url: URL(string: "/submit")) { + TextField("Your name", type: .text, name: "name") + TextField("Your email", type: .email, name: "email") + TextArea("Your message", name: "message", rows: 5) + Button(.submit) { + Text("Send Message") + } + } + } + .padding(.vertical, 48) + } + } + } +} + +let sitemap: Sitemap = [ + "contact.html": Contact() +] + +guard let projectURL = URL(filePath: #filePath)? + .deletingLastPathComponent() + .deletingLastPathComponent() else { + print("Unable to create URL for \(#filePath)") + exit(1) +} + +let outputURL = projectURL.appending(path: "site") + +try renderSitemap(sitemap, to: outputURL) diff --git a/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput-2-1.swift b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput-2-1.swift new file mode 100644 index 00000000..e6143eac --- /dev/null +++ b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput-2-1.swift @@ -0,0 +1,61 @@ +import Foundation + +import Slipstream + +struct Contact: View { + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + Container { + H1("Contact Coco") + .fontSize(.extraExtraExtraLarge) + .bold() + .margin(.bottom, 32) + + Text("Want to say hello to Coco? Fill out the form below!") + .margin(.bottom, 24) + + Form(method: .post, url: URL(string: "/submit")) { + TextField("Your name", type: .text, name: "name") + .padding(12) + .border(.all, width: 1, color: .gray, darkness: 300) + .cornerRadius(.medium) + .border(.all, width: 2, color: .blue, darkness: 500, condition: .focus) + .frame(width: .full) + + TextField("Your email", type: .email, name: "email") + .padding(12) + .border(.all, width: 1, color: .gray, darkness: 300) + .cornerRadius(.medium) + .border(.all, width: 2, color: .blue, darkness: 500, condition: .focus) + .frame(width: .full) + + TextArea("Your message", name: "message", rows: 5) + Button(.submit) { + Text("Send Message") + } + } + } + .padding(.vertical, 48) + } + } + } +} + +let sitemap: Sitemap = [ + "contact.html": Contact() +] + +guard let projectURL = URL(filePath: #filePath)? + .deletingLastPathComponent() + .deletingLastPathComponent() else { + print("Unable to create URL for \(#filePath)") + exit(1) +} + +let outputURL = projectURL.appending(path: "site") + +try renderSitemap(sitemap, to: outputURL) diff --git a/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput-2-2.swift b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput-2-2.swift new file mode 100644 index 00000000..6e2bd6f6 --- /dev/null +++ b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput-2-2.swift @@ -0,0 +1,67 @@ +import Foundation + +import Slipstream + +struct Contact: View { + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + Container { + H1("Contact Coco") + .fontSize(.extraExtraExtraLarge) + .bold() + .margin(.bottom, 32) + + Text("Want to say hello to Coco? Fill out the form below!") + .margin(.bottom, 24) + + Form(method: .post, url: URL(string: "/submit")) { + TextField("Your name", type: .text, name: "name") + .padding(12) + .border(.all, width: 1, color: .gray, darkness: 300) + .cornerRadius(.medium) + .border(.all, width: 2, color: .blue, darkness: 500, condition: .focus) + .frame(width: .full) + + TextField("Your email", type: .email, name: "email") + .padding(12) + .border(.all, width: 1, color: .gray, darkness: 300) + .cornerRadius(.medium) + .border(.all, width: 2, color: .blue, darkness: 500, condition: .focus) + .frame(width: .full) + + TextArea("Your message", name: "message", rows: 5) + .padding(12) + .border(.all, width: 1, color: .gray, darkness: 300) + .cornerRadius(.medium) + .border(.all, width: 2, color: .blue, darkness: 500, condition: .focus) + .frame(width: .full) + + Button(.submit) { + Text("Send Message") + } + } + } + .padding(.vertical, 48) + } + } + } +} + +let sitemap: Sitemap = [ + "contact.html": Contact() +] + +guard let projectURL = URL(filePath: #filePath)? + .deletingLastPathComponent() + .deletingLastPathComponent() else { + print("Unable to create URL for \(#filePath)") + exit(1) +} + +let outputURL = projectURL.appending(path: "site") + +try renderSitemap(sitemap, to: outputURL) diff --git a/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput-2-3.swift b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput-2-3.swift new file mode 100644 index 00000000..e1fc52c9 --- /dev/null +++ b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput-2-3.swift @@ -0,0 +1,74 @@ +import Foundation + +import Slipstream + +struct Contact: View { + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + Container { + H1("Contact Coco") + .fontSize(.extraExtraExtraLarge) + .bold() + .margin(.bottom, 32) + + Text("Want to say hello to Coco? Fill out the form below!") + .margin(.bottom, 24) + + Form(method: .post, url: URL(string: "/submit")) { + TextField("Your name", type: .text, name: "name") + .padding(12) + .border(.all, width: 1, color: .gray, darkness: 300) + .cornerRadius(.medium) + .border(.all, width: 2, color: .blue, darkness: 500, condition: .focus) + .frame(width: .full) + + TextField("Your email", type: .email, name: "email") + .padding(12) + .border(.all, width: 1, color: .gray, darkness: 300) + .cornerRadius(.medium) + .border(.all, width: 2, color: .blue, darkness: 500, condition: .focus) + .frame(width: .full) + + TextArea("Your message", name: "message", rows: 5) + .padding(12) + .border(.all, width: 1, color: .gray, darkness: 300) + .cornerRadius(.medium) + .border(.all, width: 2, color: .blue, darkness: 500, condition: .focus) + .frame(width: .full) + + Button(.submit) { + Text("Send Message") + } + .padding(.horizontal, 24) + .padding(.vertical, 12) + .background(.blue, darkness: 600) + .textColor(.white) + .cornerRadius(.medium) + .background(.blue, darkness: 700, condition: .hover) + .fontWeight(600) + } + } + .padding(.vertical, 48) + } + } + } +} + +let sitemap: Sitemap = [ + "contact.html": Contact() +] + +guard let projectURL = URL(filePath: #filePath)? + .deletingLastPathComponent() + .deletingLastPathComponent() else { + print("Unable to create URL for \(#filePath)") + exit(1) +} + +let outputURL = projectURL.appending(path: "site") + +try renderSitemap(sitemap, to: outputURL) diff --git a/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput-2-4.swift b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput-2-4.swift new file mode 100644 index 00000000..2054ce99 --- /dev/null +++ b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput-2-4.swift @@ -0,0 +1,76 @@ +import Foundation + +import Slipstream + +struct Contact: View { + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + Container { + H1("Contact Coco") + .fontSize(.extraExtraExtraLarge) + .bold() + .margin(.bottom, 32) + + Text("Want to say hello to Coco? Fill out the form below!") + .margin(.bottom, 24) + + Form(method: .post, url: URL(string: "/submit")) { + VStack(alignment: .leading, spacing: 16) { + TextField("Your name", type: .text, name: "name") + .padding(12) + .border(.all, width: 1, color: .gray, darkness: 300) + .cornerRadius(.medium) + .border(.all, width: 2, color: .blue, darkness: 500, condition: .focus) + .frame(width: .full) + + TextField("Your email", type: .email, name: "email") + .padding(12) + .border(.all, width: 1, color: .gray, darkness: 300) + .cornerRadius(.medium) + .border(.all, width: 2, color: .blue, darkness: 500, condition: .focus) + .frame(width: .full) + + TextArea("Your message", name: "message", rows: 5) + .padding(12) + .border(.all, width: 1, color: .gray, darkness: 300) + .cornerRadius(.medium) + .border(.all, width: 2, color: .blue, darkness: 500, condition: .focus) + .frame(width: .full) + + Button(.submit) { + Text("Send Message") + } + .padding(.horizontal, 24) + .padding(.vertical, 12) + .background(.blue, darkness: 600) + .textColor(.white) + .cornerRadius(.medium) + .background(.blue, darkness: 700, condition: .hover) + .fontWeight(600) + } + } + } + .padding(.vertical, 48) + } + } + } +} + +let sitemap: Sitemap = [ + "contact.html": Contact() +] + +guard let projectURL = URL(filePath: #filePath)? + .deletingLastPathComponent() + .deletingLastPathComponent() else { + print("Unable to create URL for \(#filePath)") + exit(1) +} + +let outputURL = projectURL.appending(path: "site") + +try renderSitemap(sitemap, to: outputURL) diff --git a/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput-3-1.swift b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput-3-1.swift new file mode 100644 index 00000000..85c68226 --- /dev/null +++ b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput-3-1.swift @@ -0,0 +1,83 @@ +import Foundation + +import Slipstream + +struct Contact: View { + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + Container { + H1("Contact Coco") + .fontSize(.extraExtraExtraLarge) + .bold() + .margin(.bottom, 32) + + Text("Want to say hello to Coco? Fill out the form below!") + .margin(.bottom, 24) + + Form(method: .post, url: URL(string: "/submit")) { + VStack(alignment: .leading, spacing: 16) { + TextField("Your name", type: .text, name: "name") + .padding(12) + .border(.all, width: 1, color: .gray, darkness: 300) + .cornerRadius(.medium) + .border(.all, width: 2, color: .blue, darkness: 500, condition: .focus) + .frame(width: .full) + + TextField("Your email", type: .email, name: "email") + .padding(12) + .border(.all, width: 1, color: .gray, darkness: 300) + .cornerRadius(.medium) + .border(.all, width: 2, color: .blue, darkness: 500, condition: .focus) + .frame(width: .full) + + TextArea("Your message", name: "message", rows: 5) + .padding(12) + .border(.all, width: 1, color: .gray, darkness: 300) + .cornerRadius(.medium) + .border(.all, width: 2, color: .blue, darkness: 500, condition: .focus) + .frame(width: .full) + + HStack(alignment: .center, spacing: 8) { + Checkbox(name: "newsletter") + Label { + Text("Subscribe to Coco's newsletter") + } + } + + Button(.submit) { + Text("Send Message") + } + .padding(.horizontal, 24) + .padding(.vertical, 12) + .background(.blue, darkness: 600) + .textColor(.white) + .cornerRadius(.medium) + .background(.blue, darkness: 700, condition: .hover) + .fontWeight(600) + } + } + } + .padding(.vertical, 48) + } + } + } +} + +let sitemap: Sitemap = [ + "contact.html": Contact() +] + +guard let projectURL = URL(filePath: #filePath)? + .deletingLastPathComponent() + .deletingLastPathComponent() else { + print("Unable to create URL for \(#filePath)") + exit(1) +} + +let outputURL = projectURL.appending(path: "site") + +try renderSitemap(sitemap, to: outputURL) diff --git a/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput-3-2.swift b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput-3-2.swift new file mode 100644 index 00000000..d9320024 --- /dev/null +++ b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput-3-2.swift @@ -0,0 +1,101 @@ +import Foundation + +import Slipstream + +struct Contact: View { + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + Container { + H1("Contact Coco") + .fontSize(.extraExtraExtraLarge) + .bold() + .margin(.bottom, 32) + + Text("Want to say hello to Coco? Fill out the form below!") + .margin(.bottom, 24) + + Form(method: .post, url: URL(string: "/submit")) { + VStack(alignment: .leading, spacing: 16) { + TextField("Your name", type: .text, name: "name") + .padding(12) + .border(.all, width: 1, color: .gray, darkness: 300) + .cornerRadius(.medium) + .border(.all, width: 2, color: .blue, darkness: 500, condition: .focus) + .frame(width: .full) + + TextField("Your email", type: .email, name: "email") + .padding(12) + .border(.all, width: 1, color: .gray, darkness: 300) + .cornerRadius(.medium) + .border(.all, width: 2, color: .blue, darkness: 500, condition: .focus) + .frame(width: .full) + + TextArea("Your message", name: "message", rows: 5) + .padding(12) + .border(.all, width: 1, color: .gray, darkness: 300) + .cornerRadius(.medium) + .border(.all, width: 2, color: .blue, darkness: 500, condition: .focus) + .frame(width: .full) + + VStack(alignment: .leading, spacing: 8) { + Label { + Text("How did you find Coco?") + .bold() + } + Picker(name: "source") { + Option("Please select", value: "") + Option("Social media", value: "social") + Option("Search engine", value: "search") + Option("Friend referral", value: "referral") + Option("Travel website", value: "travel") + } + .padding(12) + .border(.all, width: 1, color: .gray, darkness: 300) + .cornerRadius(.medium) + .frame(width: .full) + } + + HStack(alignment: .center, spacing: 8) { + Checkbox(name: "newsletter") + Label { + Text("Subscribe to Coco's newsletter") + } + } + + Button(.submit) { + Text("Send Message") + } + .padding(.horizontal, 24) + .padding(.vertical, 12) + .background(.blue, darkness: 600) + .textColor(.white) + .cornerRadius(.medium) + .background(.blue, darkness: 700, condition: .hover) + .fontWeight(600) + } + } + } + .padding(.vertical, 48) + } + } + } +} + +let sitemap: Sitemap = [ + "contact.html": Contact() +] + +guard let projectURL = URL(filePath: #filePath)? + .deletingLastPathComponent() + .deletingLastPathComponent() else { + print("Unable to create URL for \(#filePath)") + exit(1) +} + +let outputURL = projectURL.appending(path: "site") + +try renderSitemap(sitemap, to: outputURL) diff --git a/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput-3-3.swift b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput-3-3.swift new file mode 100644 index 00000000..14f39a22 --- /dev/null +++ b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput-3-3.swift @@ -0,0 +1,128 @@ +import Foundation + +import Slipstream + +struct Contact: View { + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + Container { + H1("Contact Coco") + .fontSize(.extraExtraExtraLarge) + .bold() + .margin(.bottom, 32) + + Text("Want to say hello to Coco? Fill out the form below!") + .margin(.bottom, 24) + + Form(method: .post, url: URL(string: "/submit")) { + VStack(alignment: .leading, spacing: 16) { + TextField("Your name", type: .text, name: "name") + .padding(12) + .border(.all, width: 1, color: .gray, darkness: 300) + .cornerRadius(.medium) + .border(.all, width: 2, color: .blue, darkness: 500, condition: .focus) + .frame(width: .full) + + TextField("Your email", type: .email, name: "email") + .padding(12) + .border(.all, width: 1, color: .gray, darkness: 300) + .cornerRadius(.medium) + .border(.all, width: 2, color: .blue, darkness: 500, condition: .focus) + .frame(width: .full) + + TextArea("Your message", name: "message", rows: 5) + .padding(12) + .border(.all, width: 1, color: .gray, darkness: 300) + .cornerRadius(.medium) + .border(.all, width: 2, color: .blue, darkness: 500, condition: .focus) + .frame(width: .full) + + VStack(alignment: .leading, spacing: 8) { + Label { + Text("How did you find Coco?") + .bold() + } + Picker(name: "source") { + Option("Please select", value: "") + Option("Social media", value: "social") + Option("Search engine", value: "search") + Option("Friend referral", value: "referral") + Option("Travel website", value: "travel") + } + .padding(12) + .border(.all, width: 1, color: .gray, darkness: 300) + .cornerRadius(.medium) + .frame(width: .full) + } + + VStack(alignment: .leading, spacing: 8) { + Label { + Text("What's your favorite Coco activity?") + .bold() + } + VStack(alignment: .leading, spacing: 4) { + HStack(alignment: .center, spacing: 8) { + RadioButton(value: "swimming", name: "activity") + Label { + Text("Swimming") + } + } + HStack(alignment: .center, spacing: 8) { + RadioButton(value: "beach", name: "activity") + Label { + Text("Beach lounging") + } + } + HStack(alignment: .center, spacing: 8) { + RadioButton(value: "greeting", name: "activity") + Label { + Text("Greeting tourists") + } + } + } + } + + HStack(alignment: .center, spacing: 8) { + Checkbox(name: "newsletter") + Label { + Text("Subscribe to Coco's newsletter") + } + } + + Button(.submit) { + Text("Send Message") + } + .padding(.horizontal, 24) + .padding(.vertical, 12) + .background(.blue, darkness: 600) + .textColor(.white) + .cornerRadius(.medium) + .background(.blue, darkness: 700, condition: .hover) + .fontWeight(600) + } + } + } + .padding(.vertical, 48) + } + } + } +} + +let sitemap: Sitemap = [ + "contact.html": Contact() +] + +guard let projectURL = URL(filePath: #filePath)? + .deletingLastPathComponent() + .deletingLastPathComponent() else { + print("Unable to create URL for \(#filePath)") + exit(1) +} + +let outputURL = projectURL.appending(path: "site") + +try renderSitemap(sitemap, to: outputURL) diff --git a/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput-4-1.swift b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput-4-1.swift new file mode 100644 index 00000000..cb189792 --- /dev/null +++ b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput-4-1.swift @@ -0,0 +1,134 @@ +import Foundation + +import Slipstream + +struct Contact: View { + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + Container { + H1("Contact Coco") + .fontSize(.extraExtraExtraLarge) + .bold() + .margin(.bottom, 32) + + Text("Want to say hello to Coco? Fill out the form below!") + .margin(.bottom, 24) + + Form(method: .post, url: URL(string: "/submit")) { + VStack(alignment: .leading, spacing: 24) { + Fieldset { + Legend("Contact Information") + + VStack(alignment: .leading, spacing: 16) { + TextField("Your name", type: .text, name: "name") + .padding(12) + .border(.all, width: 1, color: .gray, darkness: 300) + .cornerRadius(.medium) + .border(.all, width: 2, color: .blue, darkness: 500, condition: .focus) + .frame(width: .full) + + TextField("Your email", type: .email, name: "email") + .padding(12) + .border(.all, width: 1, color: .gray, darkness: 300) + .cornerRadius(.medium) + .border(.all, width: 2, color: .blue, darkness: 500, condition: .focus) + .frame(width: .full) + + TextArea("Your message", name: "message", rows: 5) + .padding(12) + .border(.all, width: 1, color: .gray, darkness: 300) + .cornerRadius(.medium) + .border(.all, width: 2, color: .blue, darkness: 500, condition: .focus) + .frame(width: .full) + } + } + + VStack(alignment: .leading, spacing: 8) { + Label { + Text("How did you find Coco?") + .bold() + } + Picker(name: "source") { + Option("Please select", value: "") + Option("Social media", value: "social") + Option("Search engine", value: "search") + Option("Friend referral", value: "referral") + Option("Travel website", value: "travel") + } + .padding(12) + .border(.all, width: 1, color: .gray, darkness: 300) + .cornerRadius(.medium) + .frame(width: .full) + } + + VStack(alignment: .leading, spacing: 8) { + Label { + Text("What's your favorite Coco activity?") + .bold() + } + VStack(alignment: .leading, spacing: 4) { + HStack(alignment: .center, spacing: 8) { + RadioButton(value: "swimming", name: "activity") + Label { + Text("Swimming") + } + } + HStack(alignment: .center, spacing: 8) { + RadioButton(value: "beach", name: "activity") + Label { + Text("Beach lounging") + } + } + HStack(alignment: .center, spacing: 8) { + RadioButton(value: "greeting", name: "activity") + Label { + Text("Greeting tourists") + } + } + } + } + + HStack(alignment: .center, spacing: 8) { + Checkbox(name: "newsletter") + Label { + Text("Subscribe to Coco's newsletter") + } + } + + Button(.submit) { + Text("Send Message") + } + .padding(.horizontal, 24) + .padding(.vertical, 12) + .background(.blue, darkness: 600) + .textColor(.white) + .cornerRadius(.medium) + .background(.blue, darkness: 700, condition: .hover) + .fontWeight(600) + } + } + } + .padding(.vertical, 48) + } + } + } +} + +let sitemap: Sitemap = [ + "contact.html": Contact() +] + +guard let projectURL = URL(filePath: #filePath)? + .deletingLastPathComponent() + .deletingLastPathComponent() else { + print("Unable to create URL for \(#filePath)") + exit(1) +} + +let outputURL = projectURL.appending(path: "site") + +try renderSitemap(sitemap, to: outputURL) diff --git a/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput-4-2.swift b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput-4-2.swift new file mode 100644 index 00000000..815e5997 --- /dev/null +++ b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput-4-2.swift @@ -0,0 +1,140 @@ +import Foundation + +import Slipstream + +struct Contact: View { + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + Container { + H1("Contact Coco") + .fontSize(.extraExtraExtraLarge) + .bold() + .margin(.bottom, 32) + + Text("Want to say hello to Coco? Fill out the form below!") + .margin(.bottom, 24) + + Form(method: .post, url: URL(string: "/submit")) { + VStack(alignment: .leading, spacing: 24) { + Fieldset { + Legend("Contact Information") + + VStack(alignment: .leading, spacing: 16) { + TextField("Your name", type: .text, name: "name") + .padding(12) + .border(.all, width: 1, color: .gray, darkness: 300) + .cornerRadius(.medium) + .border(.all, width: 2, color: .blue, darkness: 500, condition: .focus) + .frame(width: .full) + + TextField("Your email", type: .email, name: "email") + .padding(12) + .border(.all, width: 1, color: .gray, darkness: 300) + .cornerRadius(.medium) + .border(.all, width: 2, color: .blue, darkness: 500, condition: .focus) + .frame(width: .full) + + TextArea("Your message", name: "message", rows: 5) + .padding(12) + .border(.all, width: 1, color: .gray, darkness: 300) + .cornerRadius(.medium) + .border(.all, width: 2, color: .blue, darkness: 500, condition: .focus) + .frame(width: .full) + } + } + + Fieldset { + Legend("Preferences") + + VStack(alignment: .leading, spacing: 16) { + VStack(alignment: .leading, spacing: 8) { + Label { + Text("How did you find Coco?") + .bold() + } + Picker(name: "source") { + Option("Please select", value: "") + Option("Social media", value: "social") + Option("Search engine", value: "search") + Option("Friend referral", value: "referral") + Option("Travel website", value: "travel") + } + .padding(12) + .border(.all, width: 1, color: .gray, darkness: 300) + .cornerRadius(.medium) + .frame(width: .full) + } + + VStack(alignment: .leading, spacing: 8) { + Label { + Text("What's your favorite Coco activity?") + .bold() + } + VStack(alignment: .leading, spacing: 4) { + HStack(alignment: .center, spacing: 8) { + RadioButton(value: "swimming", name: "activity") + Label { + Text("Swimming") + } + } + HStack(alignment: .center, spacing: 8) { + RadioButton(value: "beach", name: "activity") + Label { + Text("Beach lounging") + } + } + HStack(alignment: .center, spacing: 8) { + RadioButton(value: "greeting", name: "activity") + Label { + Text("Greeting tourists") + } + } + } + } + + HStack(alignment: .center, spacing: 8) { + Checkbox(name: "newsletter") + Label { + Text("Subscribe to Coco's newsletter") + } + } + } + } + + Button(.submit) { + Text("Send Message") + } + .padding(.horizontal, 24) + .padding(.vertical, 12) + .background(.blue, darkness: 600) + .textColor(.white) + .cornerRadius(.medium) + .background(.blue, darkness: 700, condition: .hover) + .fontWeight(600) + } + } + } + .padding(.vertical, 48) + } + } + } +} + +let sitemap: Sitemap = [ + "contact.html": Contact() +] + +guard let projectURL = URL(filePath: #filePath)? + .deletingLastPathComponent() + .deletingLastPathComponent() else { + print("Unable to create URL for \(#filePath)") + exit(1) +} + +let outputURL = projectURL.appending(path: "site") + +try renderSitemap(sitemap, to: outputURL) diff --git a/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput-4-3.swift b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput-4-3.swift new file mode 100644 index 00000000..a78a0ae4 --- /dev/null +++ b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput-4-3.swift @@ -0,0 +1,154 @@ +import Foundation + +import Slipstream + +struct Contact: View { + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + Container { + H1("Contact Coco") + .fontSize(.extraExtraExtraLarge) + .bold() + .margin(.bottom, 32) + + Text("Want to say hello to Coco? Fill out the form below!") + .margin(.bottom, 24) + + Form(method: .post, url: URL(string: "/submit")) { + VStack(alignment: .leading, spacing: 24) { + Fieldset { + Legend("Contact Information") + .bold() + .fontSize(.large) + .margin(.bottom, 16) + + VStack(alignment: .leading, spacing: 16) { + TextField("Your name", type: .text, name: "name") + .padding(12) + .border(.all, width: 1, color: .gray, darkness: 300) + .cornerRadius(.medium) + .border(.all, width: 2, color: .blue, darkness: 500, condition: .focus) + .frame(width: .full) + + TextField("Your email", type: .email, name: "email") + .padding(12) + .border(.all, width: 1, color: .gray, darkness: 300) + .cornerRadius(.medium) + .border(.all, width: 2, color: .blue, darkness: 500, condition: .focus) + .frame(width: .full) + + TextArea("Your message", name: "message", rows: 5) + .padding(12) + .border(.all, width: 1, color: .gray, darkness: 300) + .cornerRadius(.medium) + .border(.all, width: 2, color: .blue, darkness: 500, condition: .focus) + .frame(width: .full) + } + } + .padding(20) + .border(.all, width: 1, color: .gray, darkness: 200) + .cornerRadius(.large) + .background(.gray, darkness: 50) + + Fieldset { + Legend("Preferences") + .bold() + .fontSize(.large) + .margin(.bottom, 16) + + VStack(alignment: .leading, spacing: 16) { + VStack(alignment: .leading, spacing: 8) { + Label { + Text("How did you find Coco?") + .bold() + } + Picker(name: "source") { + Option("Please select", value: "") + Option("Social media", value: "social") + Option("Search engine", value: "search") + Option("Friend referral", value: "referral") + Option("Travel website", value: "travel") + } + .padding(12) + .border(.all, width: 1, color: .gray, darkness: 300) + .cornerRadius(.medium) + .frame(width: .full) + } + + VStack(alignment: .leading, spacing: 8) { + Label { + Text("What's your favorite Coco activity?") + .bold() + } + VStack(alignment: .leading, spacing: 4) { + HStack(alignment: .center, spacing: 8) { + RadioButton(value: "swimming", name: "activity") + Label { + Text("Swimming") + } + } + HStack(alignment: .center, spacing: 8) { + RadioButton(value: "beach", name: "activity") + Label { + Text("Beach lounging") + } + } + HStack(alignment: .center, spacing: 8) { + RadioButton(value: "greeting", name: "activity") + Label { + Text("Greeting tourists") + } + } + } + } + + HStack(alignment: .center, spacing: 8) { + Checkbox(name: "newsletter") + Label { + Text("Subscribe to Coco's newsletter") + } + } + } + } + .padding(20) + .border(.all, width: 1, color: .gray, darkness: 200) + .cornerRadius(.large) + .background(.gray, darkness: 50) + + Button(.submit) { + Text("Send Message") + } + .padding(.horizontal, 24) + .padding(.vertical, 12) + .background(.blue, darkness: 600) + .textColor(.white) + .cornerRadius(.medium) + .background(.blue, darkness: 700, condition: .hover) + .fontWeight(600) + } + } + } + .padding(.vertical, 48) + } + } + } +} + +let sitemap: Sitemap = [ + "contact.html": Contact() +] + +guard let projectURL = URL(filePath: #filePath)? + .deletingLastPathComponent() + .deletingLastPathComponent() else { + print("Unable to create URL for \(#filePath)") + exit(1) +} + +let outputURL = projectURL.appending(path: "site") + +try renderSitemap(sitemap, to: outputURL) diff --git a/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput.tutorial b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput.tutorial new file mode 100644 index 00000000..e93b2e9c --- /dev/null +++ b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/FormsAndInput/IntroducingSlipstream-YourFirstSite-FormsAndInput.tutorial @@ -0,0 +1,191 @@ +@Tutorial(time: 20) { + @XcodeRequirement(title: "Xcode 15 or later", destination: "https://itunes.apple.com/us/app/xcode/id497799835?mt=12") + + @Intro(title: "Forms and User Input") { + Learn how to build interactive forms with various input types, validation, and styling. + + This tutorial will show you how to create a contact form with text inputs, email validation, checkboxes, and submit buttons. + + @Image(source: "tutorial-banner", alt: "Coco the pig") + } + + @Section(title: "Creating a basic form") { + @ContentAndMedia { + Forms are essential for collecting user input on websites. Slipstream provides comprehensive form support with all standard HTML input types. + + Let's build a contact form for Coco's website. + + @Image(source: "logo-square", alt: "The Slipstream logo") + } + + @Steps { + @Step { + Start with a basic page structure. We'll add a contact form to let visitors get in touch. + + @Code(name: "main.swift", file: "IntroducingSlipstream-YourFirstSite-FormsAndInput-1-1.swift") + } + + @Step { + Create a ``Form`` with the POST method. Forms in Slipstream support GET, POST, and dialog methods. + + The `url` parameter specifies where the form data should be sent when submitted. + + @Code(name: "main.swift", file: "IntroducingSlipstream-YourFirstSite-FormsAndInput-1-2.swift") + } + + @Step { + Add ``TextField`` inputs for the user's name and email. Each input should have a descriptive placeholder and a unique `name` attribute. + + The `type` parameter lets you specify different input types like `.text`, `.email`, `.password`, `.tel`, `.url`, and more. + + @Code(name: "main.swift", file: "IntroducingSlipstream-YourFirstSite-FormsAndInput-1-3.swift") { + @Image(source: "IntroducingSlipstream-YourFirstSite-FormsAndInput-1-3-preview", alt: "Basic form with name and email fields") + } + } + + @Step { + Add a ``TextArea`` for the message. TextArea is perfect for multi-line text input. + + @Code(name: "main.swift", file: "IntroducingSlipstream-YourFirstSite-FormsAndInput-1-4.swift") { + @Image(source: "IntroducingSlipstream-YourFirstSite-FormsAndInput-1-4-preview", alt: "Form with textarea") + } + } + + @Step { + Add a submit button using ``Button``. The button should be inside the form to submit it when clicked. + + @Code(name: "main.swift", file: "IntroducingSlipstream-YourFirstSite-FormsAndInput-1-5.swift") { + @Image(source: "IntroducingSlipstream-YourFirstSite-FormsAndInput-1-5-preview", alt: "Complete basic form") + } + } + } + } + + @Section(title: "Styling form elements") { + @ContentAndMedia { + Default form elements can look plain. Let's add Tailwind CSS styling to make our form more attractive and user-friendly. + + @Image(source: "logo-square", alt: "The Slipstream logo") + } + + @Steps { + @Step { + Add styling to the text fields. We'll add padding, borders, rounded corners, and focus states. + + The `condition: .focus` parameter applies styles when the input is focused. + + @Code(name: "main.swift", file: "IntroducingSlipstream-YourFirstSite-FormsAndInput-2-1.swift", previousFile: "IntroducingSlipstream-YourFirstSite-FormsAndInput-1-5.swift") + } + + @Step { + Style the textarea with the same design system for consistency. + + @Code(name: "main.swift", file: "IntroducingSlipstream-YourFirstSite-FormsAndInput-2-2.swift") + } + + @Step { + Style the submit button to make it stand out. We'll add a blue background, white text, hover effects, and a transition. + + @Code(name: "main.swift", file: "IntroducingSlipstream-YourFirstSite-FormsAndInput-2-3.swift") { + @Image(source: "IntroducingSlipstream-YourFirstSite-FormsAndInput-2-3-preview", alt: "Beautifully styled form") + } + } + + @Step { + Organize the form with proper spacing using a ``VStack``. This creates consistent vertical spacing between form elements. + + @Code(name: "main.swift", file: "IntroducingSlipstream-YourFirstSite-FormsAndInput-2-4.swift") { + @Image(source: "IntroducingSlipstream-YourFirstSite-FormsAndInput-2-4-preview", alt: "Well-organized form with spacing") + } + } + } + } + + @Section(title: "Adding more input types") { + @ContentAndMedia { + Slipstream supports many input types beyond basic text fields. Let's explore checkboxes, radio buttons, and select dropdowns. + + @Image(source: "logo-square", alt: "The Slipstream logo") + } + + @Steps { + @Step { + Add a ``Checkbox`` to let users opt in to a newsletter. Checkboxes are great for yes/no options. + + Use ``Label`` to associate descriptive text with form controls for better accessibility. + + @Code(name: "main.swift", file: "IntroducingSlipstream-YourFirstSite-FormsAndInput-3-1.swift", previousFile: "IntroducingSlipstream-YourFirstSite-FormsAndInput-2-4.swift") + } + + @Step { + Add a ``Picker`` (select dropdown) to let users choose how they found Coco's website. + + Use ``Option`` elements inside the Picker for each choice. + + @Code(name: "main.swift", file: "IntroducingSlipstream-YourFirstSite-FormsAndInput-3-2.swift") + } + + @Step { + Add ``RadioButton`` elements to let users select their favorite Coco activity. Radio buttons are perfect for mutually exclusive choices. + + All radio buttons in a group should share the same `name` attribute. + + @Code(name: "main.swift", file: "IntroducingSlipstream-YourFirstSite-FormsAndInput-3-3.swift") { + @Image(source: "IntroducingSlipstream-YourFirstSite-FormsAndInput-3-3-preview", alt: "Form with various input types") + } + } + } + } + + @Section(title: "Using Fieldset for organization") { + @ContentAndMedia { + For complex forms, ``Fieldset`` and ``Legend`` help organize related fields into logical groups. + + This improves both the visual organization and accessibility of your forms. + + @Image(source: "logo-square", alt: "The Slipstream logo") + } + + @Steps { + @Step { + Group the contact information fields into a ``Fieldset`` with a ``Legend``. + + @Code(name: "main.swift", file: "IntroducingSlipstream-YourFirstSite-FormsAndInput-4-1.swift", previousFile: "IntroducingSlipstream-YourFirstSite-FormsAndInput-3-3.swift") + } + + @Step { + Create another fieldset for preferences. This makes the form easier to scan and understand. + + @Code(name: "main.swift", file: "IntroducingSlipstream-YourFirstSite-FormsAndInput-4-2.swift") + } + + @Step { + Add styling to the fieldsets to visually separate the sections. + + @Code(name: "main.swift", file: "IntroducingSlipstream-YourFirstSite-FormsAndInput-4-3.swift") { + @Image(source: "IntroducingSlipstream-YourFirstSite-FormsAndInput-4-3-preview", alt: "Organized form with fieldsets") + } + } + + @Step { + Congratulations! You've built a comprehensive contact form with multiple input types and proper organization. + + You've learned how to: + - Create forms with ``Form``, ``TextField``, and ``TextArea`` + - Style form elements with Tailwind CSS modifiers + - Use different input types: ``Checkbox``, ``Picker``, and ``RadioButton`` + - Organize forms with ``Fieldset`` and ``Legend`` + - Apply focus states and transitions for better UX + + Next steps to explore: + - Form validation with HTML5 attributes (required, pattern, min, max) + - Other input types like ``Slider``, ``ColorPicker``, and ``FileInput`` + - Custom form components with ``Label`` and proper accessibility + - Client-side interactivity with ``Script`` for dynamic forms + - Server-side form handling with your backend of choice + + @Code(name: "main.swift", file: "IntroducingSlipstream-YourFirstSite-FormsAndInput-4-3.swift") + } + } + } +} diff --git a/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/LayoutsAndResponsive/IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-1-1.swift b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/LayoutsAndResponsive/IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-1-1.swift new file mode 100644 index 00000000..8fb8dd98 --- /dev/null +++ b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/LayoutsAndResponsive/IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-1-1.swift @@ -0,0 +1,33 @@ +import Foundation + +import Slipstream + +struct Home: View { + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + Container { + Text("Coco the Swimming Pig") + } + } + } + } +} + +let sitemap: Sitemap = [ + "index.html": Home() +] + +guard let projectURL = URL(filePath: #filePath)? + .deletingLastPathComponent() + .deletingLastPathComponent() else { + print("Unable to create URL for \(#filePath)") + exit(1) +} + +let outputURL = projectURL.appending(path: "site") + +try renderSitemap(sitemap, to: outputURL) diff --git a/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/LayoutsAndResponsive/IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-1-2.swift b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/LayoutsAndResponsive/IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-1-2.swift new file mode 100644 index 00000000..abba6dbd --- /dev/null +++ b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/LayoutsAndResponsive/IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-1-2.swift @@ -0,0 +1,37 @@ +import Foundation + +import Slipstream + +struct Home: View { + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + Container { + VStack(alignment: .leading, spacing: 16) { + Text("Coco the Swimming Pig") + Text("Coco lives in the Bahamas and loves to swim in the crystal-clear waters.") + Image(URL(string: "coco.jpg")) + } + } + } + } + } +} + +let sitemap: Sitemap = [ + "index.html": Home() +] + +guard let projectURL = URL(filePath: #filePath)? + .deletingLastPathComponent() + .deletingLastPathComponent() else { + print("Unable to create URL for \(#filePath)") + exit(1) +} + +let outputURL = projectURL.appending(path: "site") + +try renderSitemap(sitemap, to: outputURL) diff --git a/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/LayoutsAndResponsive/IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-1-3.swift b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/LayoutsAndResponsive/IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-1-3.swift new file mode 100644 index 00000000..1b89fa5c --- /dev/null +++ b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/LayoutsAndResponsive/IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-1-3.swift @@ -0,0 +1,54 @@ +import Foundation + +import Slipstream + +struct Home: View { + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + Container { + VStack(alignment: .leading, spacing: 16) { + Text("Coco the Swimming Pig") + Text("Coco lives in the Bahamas and loves to swim in the crystal-clear waters.") + Image(URL(string: "coco.jpg")) + HStack(alignment: .center, spacing: 24) { + VStack(alignment: .leading, spacing: 4) { + Text("Location") + .bold() + Text("Bahamas") + } + VStack(alignment: .leading, spacing: 4) { + Text("Activity") + .bold() + Text("Swimming") + } + VStack(alignment: .leading, spacing: 4) { + Text("Species") + .bold() + Text("Pig") + } + } + } + } + } + } + } +} + +let sitemap: Sitemap = [ + "index.html": Home() +] + +guard let projectURL = URL(filePath: #filePath)? + .deletingLastPathComponent() + .deletingLastPathComponent() else { + print("Unable to create URL for \(#filePath)") + exit(1) +} + +let outputURL = projectURL.appending(path: "site") + +try renderSitemap(sitemap, to: outputURL) diff --git a/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/LayoutsAndResponsive/IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-1-4.swift b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/LayoutsAndResponsive/IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-1-4.swift new file mode 100644 index 00000000..57936e47 --- /dev/null +++ b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/LayoutsAndResponsive/IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-1-4.swift @@ -0,0 +1,65 @@ +import Foundation + +import Slipstream + +struct Home: View { + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + Container { + VStack(alignment: .leading, spacing: 16) { + Text("Coco the Swimming Pig") + .fontSize(.extraExtraLarge) + .bold() + Text("Coco lives in the Bahamas and loves to swim in the crystal-clear waters.") + Image(URL(string: "coco.jpg")) + .cornerRadius(.large) + HStack(alignment: .center, spacing: 24) { + VStack(alignment: .leading, spacing: 4) { + Text("Location") + .bold() + .textColor(.gray, darkness: 600) + Text("Bahamas") + } + VStack(alignment: .leading, spacing: 4) { + Text("Activity") + .bold() + .textColor(.gray, darkness: 600) + Text("Swimming") + } + VStack(alignment: .leading, spacing: 4) { + Text("Species") + .bold() + .textColor(.gray, darkness: 600) + Text("Pig") + } + } + } + .padding(24) + .background(.white) + .cornerRadius(.extraLarge) + .shadow(.extraLarge) + } + .padding(.vertical, 48) + } + } + } +} + +let sitemap: Sitemap = [ + "index.html": Home() +] + +guard let projectURL = URL(filePath: #filePath)? + .deletingLastPathComponent() + .deletingLastPathComponent() else { + print("Unable to create URL for \(#filePath)") + exit(1) +} + +let outputURL = projectURL.appending(path: "site") + +try renderSitemap(sitemap, to: outputURL) diff --git a/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/LayoutsAndResponsive/IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-2-1.swift b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/LayoutsAndResponsive/IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-2-1.swift new file mode 100644 index 00000000..ec37a0a6 --- /dev/null +++ b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/LayoutsAndResponsive/IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-2-1.swift @@ -0,0 +1,67 @@ +import Foundation + +import Slipstream + +struct Home: View { + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + Container { + ResponsiveStack(.x, condition: .startingAt(.medium), spacing: 32) { + Image(URL(string: "coco.jpg")) + .cornerRadius(.large) + VStack(alignment: .leading, spacing: 16) { + Text("Coco the Swimming Pig") + .fontSize(.extraExtraLarge) + .bold() + Text("Coco lives in the Bahamas and loves to swim in the crystal-clear waters.") + HStack(alignment: .center, spacing: 24) { + VStack(alignment: .leading, spacing: 4) { + Text("Location") + .bold() + .textColor(.gray, darkness: 600) + Text("Bahamas") + } + VStack(alignment: .leading, spacing: 4) { + Text("Activity") + .bold() + .textColor(.gray, darkness: 600) + Text("Swimming") + } + VStack(alignment: .leading, spacing: 4) { + Text("Species") + .bold() + .textColor(.gray, darkness: 600) + Text("Pig") + } + } + } + } + .padding(24) + .background(.white) + .cornerRadius(.extraLarge) + .shadow(.extraLarge) + } + .padding(.vertical, 48) + } + } + } +} + +let sitemap: Sitemap = [ + "index.html": Home() +] + +guard let projectURL = URL(filePath: #filePath)? + .deletingLastPathComponent() + .deletingLastPathComponent() else { + print("Unable to create URL for \(#filePath)") + exit(1) +} + +let outputURL = projectURL.appending(path: "site") + +try renderSitemap(sitemap, to: outputURL) diff --git a/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/LayoutsAndResponsive/IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-2-2.swift b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/LayoutsAndResponsive/IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-2-2.swift new file mode 100644 index 00000000..6611e423 --- /dev/null +++ b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/LayoutsAndResponsive/IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-2-2.swift @@ -0,0 +1,69 @@ +import Foundation + +import Slipstream + +struct Home: View { + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + Container { + ResponsiveStack(.x, condition: .startingAt(.medium), spacing: 32) { + Image(URL(string: "coco.jpg")) + .cornerRadius(.large) + VStack(alignment: .leading, spacing: 16) { + Text("Coco the Swimming Pig") + .fontSize(.extraExtraLarge) + .fontSize(.extraExtraExtraLarge, condition: .startingAt(.large)) + .bold() + Text("Coco lives in the Bahamas and loves to swim in the crystal-clear waters.") + HStack(alignment: .center, spacing: 24) { + VStack(alignment: .leading, spacing: 4) { + Text("Location") + .bold() + .textColor(.gray, darkness: 600) + Text("Bahamas") + } + VStack(alignment: .leading, spacing: 4) { + Text("Activity") + .bold() + .textColor(.gray, darkness: 600) + Text("Swimming") + } + VStack(alignment: .leading, spacing: 4) { + Text("Species") + .bold() + .textColor(.gray, darkness: 600) + Text("Pig") + } + } + } + } + .padding(16) + .padding(24, condition: .startingAt(.medium)) + .background(.white) + .cornerRadius(.extraLarge) + .shadow(.extraLarge) + } + .padding(.vertical, 48) + } + } + } +} + +let sitemap: Sitemap = [ + "index.html": Home() +] + +guard let projectURL = URL(filePath: #filePath)? + .deletingLastPathComponent() + .deletingLastPathComponent() else { + print("Unable to create URL for \(#filePath)") + exit(1) +} + +let outputURL = projectURL.appending(path: "site") + +try renderSitemap(sitemap, to: outputURL) diff --git a/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/LayoutsAndResponsive/IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-2-3.swift b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/LayoutsAndResponsive/IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-2-3.swift new file mode 100644 index 00000000..8028bcdc --- /dev/null +++ b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/LayoutsAndResponsive/IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-2-3.swift @@ -0,0 +1,71 @@ +import Foundation + +import Slipstream + +struct Home: View { + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + Container { + ResponsiveStack(.x, condition: .startingAt(.medium), spacing: 32) { + Image(URL(string: "coco.jpg")) + .frame(width: .full) + .frame(width: 300, condition: .startingAt(.medium)) + .cornerRadius(.large) + VStack(alignment: .leading, spacing: 16) { + Text("Coco the Swimming Pig") + .fontSize(.extraExtraLarge) + .fontSize(.extraExtraExtraLarge, condition: .startingAt(.large)) + .bold() + Text("Coco lives in the Bahamas and loves to swim in the crystal-clear waters.") + HStack(alignment: .center, spacing: 24) { + VStack(alignment: .leading, spacing: 4) { + Text("Location") + .bold() + .textColor(.gray, darkness: 600) + Text("Bahamas") + } + VStack(alignment: .leading, spacing: 4) { + Text("Activity") + .bold() + .textColor(.gray, darkness: 600) + Text("Swimming") + } + VStack(alignment: .leading, spacing: 4) { + Text("Species") + .bold() + .textColor(.gray, darkness: 600) + Text("Pig") + } + } + } + } + .padding(16) + .padding(24, condition: .startingAt(.medium)) + .background(.white) + .cornerRadius(.extraLarge) + .shadow(.extraLarge) + } + .padding(.vertical, 48) + } + } + } +} + +let sitemap: Sitemap = [ + "index.html": Home() +] + +guard let projectURL = URL(filePath: #filePath)? + .deletingLastPathComponent() + .deletingLastPathComponent() else { + print("Unable to create URL for \(#filePath)") + exit(1) +} + +let outputURL = projectURL.appending(path: "site") + +try renderSitemap(sitemap, to: outputURL) diff --git a/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/LayoutsAndResponsive/IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-3-1.swift b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/LayoutsAndResponsive/IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-3-1.swift new file mode 100644 index 00000000..63cfbdf1 --- /dev/null +++ b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/LayoutsAndResponsive/IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-3-1.swift @@ -0,0 +1,44 @@ +import Foundation + +import Slipstream + +struct Home: View { + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + Container { + H1("Coco's Photo Gallery") + .bold() + .fontSize(.extraExtraExtraLarge) + .margin(.bottom, 32) + + Div { + // Gallery items will go here + } + .display(.grid) + .gridTemplateColumns(2) + .flexGap(16) + } + .padding(.vertical, 48) + } + } + } +} + +let sitemap: Sitemap = [ + "index.html": Home() +] + +guard let projectURL = URL(filePath: #filePath)? + .deletingLastPathComponent() + .deletingLastPathComponent() else { + print("Unable to create URL for \(#filePath)") + exit(1) +} + +let outputURL = projectURL.appending(path: "site") + +try renderSitemap(sitemap, to: outputURL) diff --git a/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/LayoutsAndResponsive/IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-3-2.swift b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/LayoutsAndResponsive/IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-3-2.swift new file mode 100644 index 00000000..e6c86ead --- /dev/null +++ b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/LayoutsAndResponsive/IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-3-2.swift @@ -0,0 +1,46 @@ +import Foundation + +import Slipstream + +struct Home: View { + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + Container { + H1("Coco's Photo Gallery") + .bold() + .fontSize(.extraExtraExtraLarge) + .margin(.bottom, 32) + + Div { + // Gallery items will go here + } + .display(.grid) + .gridTemplateColumns(2) + .gridTemplateColumns(3, condition: .startingAt(.medium)) + .gridTemplateColumns(4, condition: .startingAt(.large)) + .flexGap(16) + } + .padding(.vertical, 48) + } + } + } +} + +let sitemap: Sitemap = [ + "index.html": Home() +] + +guard let projectURL = URL(filePath: #filePath)? + .deletingLastPathComponent() + .deletingLastPathComponent() else { + print("Unable to create URL for \(#filePath)") + exit(1) +} + +let outputURL = projectURL.appending(path: "site") + +try renderSitemap(sitemap, to: outputURL) diff --git a/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/LayoutsAndResponsive/IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-3-3.swift b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/LayoutsAndResponsive/IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-3-3.swift new file mode 100644 index 00000000..9d60e4b3 --- /dev/null +++ b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/LayoutsAndResponsive/IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-3-3.swift @@ -0,0 +1,63 @@ +import Foundation + +import Slipstream + +struct Home: View { + let photos = [ + "coco-swimming.jpg", + "coco-beach.jpg", + "coco-friends.jpg", + "coco-sunset.jpg", + "coco-playing.jpg", + "coco-eating.jpg", + "coco-sleeping.jpg", + "coco-running.jpg" + ] + + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + Container { + H1("Coco's Photo Gallery") + .bold() + .fontSize(.extraExtraExtraLarge) + .margin(.bottom, 32) + + Div { + ForEach(photos, id: \.self) { photo in + Image(URL(string: photo)) + .cornerRadius(.large) + .frame(width: .full) + .aspectRatio(.square) + .objectFit(.cover) + } + } + .display(.grid) + .gridTemplateColumns(2) + .gridTemplateColumns(3, condition: .startingAt(.medium)) + .gridTemplateColumns(4, condition: .startingAt(.large)) + .flexGap(16) + } + .padding(.vertical, 48) + } + } + } +} + +let sitemap: Sitemap = [ + "index.html": Home() +] + +guard let projectURL = URL(filePath: #filePath)? + .deletingLastPathComponent() + .deletingLastPathComponent() else { + print("Unable to create URL for \(#filePath)") + exit(1) +} + +let outputURL = projectURL.appending(path: "site") + +try renderSitemap(sitemap, to: outputURL) diff --git a/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/LayoutsAndResponsive/IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive.tutorial b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/LayoutsAndResponsive/IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive.tutorial new file mode 100644 index 00000000..adde7d65 --- /dev/null +++ b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/LayoutsAndResponsive/IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive.tutorial @@ -0,0 +1,149 @@ +@Tutorial(time: 15) { + @XcodeRequirement(title: "Xcode 15 or later", destination: "https://itunes.apple.com/us/app/xcode/id497799835?mt=12") + + @Intro(title: "Layouts and Responsive Design") { + Learn how to create flexible layouts that adapt to different screen sizes using HStack, VStack, and responsive design patterns. + + This tutorial will teach you how to arrange content horizontally and vertically, create responsive layouts that change based on screen size, and use Tailwind's breakpoint system for mobile-first design. + + @Image(source: "tutorial-banner", alt: "Coco the pig") + } + + @Section(title: "Horizontal and vertical stacks") { + @ContentAndMedia { + Like SwiftUI, Slipstream provides ``HStack`` and ``VStack`` for arranging views horizontally and vertically. + + These stacks use CSS flexbox under the hood, giving you powerful and familiar layout tools. + + @Image(source: "logo-square", alt: "The Slipstream logo") + } + + @Steps { + @Step { + Let's start with a basic page structure. We'll build a simple card layout for Coco's profile. + + @Code(name: "main.swift", file: "IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-1-1.swift") + } + + @Step { + Add a ``VStack`` to arrange our content vertically. VStack stacks views from top to bottom, just like in SwiftUI. + + Notice we can specify spacing between items and alignment (leading, center, or trailing). + + @Code(name: "main.swift", file: "IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-1-2.swift") + } + + @Step { + Now let's add an ``HStack`` at the bottom to display some information side by side. + + HStack arranges views horizontally and supports alignment options like top, center, bottom, and baseline. + + @Code(name: "main.swift", file: "IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-1-3.swift") { + @Image(source: "IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-1-3-preview", alt: "A card layout with vertical and horizontal stacks") + } + } + + @Step { + Let's add some styling to make our card look better. We'll add padding, background color, corner radius, and a shadow. + + @Code(name: "main.swift", file: "IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-1-4.swift") { + @Image(source: "IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-1-4-preview", alt: "A styled card with shadow and rounded corners") + } + } + } + } + + @Section(title: "Responsive design with breakpoints") { + @ContentAndMedia { + Tailwind CSS provides a mobile-first breakpoint system that makes it easy to create responsive designs. + + Slipstream exposes these breakpoints through the `condition:` parameter on view modifiers. + + @Image(source: "logo-square", alt: "The Slipstream logo") + } + + @Steps { + @Step { + Let's make our layout responsive. On small screens (mobile), we want our content to stack vertically. On larger screens (tablets and desktops), we want to display the image and content side by side. + + We'll use ``ResponsiveStack`` which automatically switches between horizontal and vertical layouts based on screen size. + + @Code(name: "main.swift", file: "IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-2-1.swift", previousFile: "IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-1-4.swift") + } + + @Step { + We can also use breakpoints with view modifiers. Let's make the text larger on desktop screens and adjust padding based on screen size. + + The ``Breakpoint/medium`` breakpoint (768px) is commonly used to differentiate between mobile and tablet/desktop layouts. + + @Code(name: "main.swift", file: "IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-2-2.swift") + } + + @Step { + Let's make the image responsive too. On mobile, it should take full width. On larger screens, it should have a fixed width. + + Notice how we can chain conditions to create sophisticated responsive designs. + + @Code(name: "main.swift", file: "IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-2-3.swift") { + @Image(source: "IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-2-3-preview", alt: "A responsive layout adapting to screen size") + } + } + + @Step { + Try resizing your browser window to see the layout change! On mobile (< 768px), everything stacks vertically. On larger screens, the image and content appear side by side. + + You can also test this by opening your browser's developer tools (Cmd+Option+I) and toggling device emulation. + + @Image(source: "IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-2-4", alt: "Testing responsive design in browser") + } + } + } + + @Section(title: "Building a responsive grid") { + @ContentAndMedia { + For more complex layouts, you can use CSS Grid through Slipstream's display and grid modifiers. + + Let's create a photo gallery that shows different numbers of columns based on screen size. + + @Image(source: "logo-square", alt: "The Slipstream logo") + } + + @Steps { + @Step { + Create a container with grid display. We'll start with a simple grid that shows 2 columns on mobile. + + @Code(name: "main.swift", file: "IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-3-1.swift", previousFile: "IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-2-3.swift") + } + + @Step { + Add more columns for larger screens. On tablets (medium), we'll show 3 columns, and on desktops (large), we'll show 4 columns. + + The `gridTemplateColumns(_:condition:)` modifier lets us define how many columns our grid should have. + + @Code(name: "main.swift", file: "IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-3-2.swift") + } + + @Step { + Let's add some sample images to our gallery using ``ForEach``. + + @Code(name: "main.swift", file: "IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-3-3.swift") { + @Image(source: "IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-3-3-preview", alt: "A responsive image gallery grid") + } + } + + @Step { + Congratulations! You now know how to create flexible, responsive layouts with Slipstream. + + You've learned: + - Using HStack and VStack for basic layouts + - Creating responsive designs with breakpoints + - Using ResponsiveStack for adaptive layouts + - Building responsive grids with CSS Grid + + For more layout options, explore the ``View/display(_:condition:)`` modifier, flexbox utilities like ``View/justifyContent(_:condition:)`` and ``View/alignItems(_:condition:)``, and grid utilities like ``View/gridCellColumns(_:condition:)`` and ``View/gridCellRows(_:condition:)``. + + @Code(name: "main.swift", file: "IntroducingSlipstream-YourFirstSite-LayoutsAndResponsive-3-3.swift") + } + } + } +} diff --git a/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/MultiPageSite/IntroducingSlipstream-YourFirstSite-MultiPageSite-1-1.swift b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/MultiPageSite/IntroducingSlipstream-YourFirstSite-MultiPageSite-1-1.swift new file mode 100644 index 00000000..8804617c --- /dev/null +++ b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/MultiPageSite/IntroducingSlipstream-YourFirstSite-MultiPageSite-1-1.swift @@ -0,0 +1,37 @@ +import Foundation + +import Slipstream + +struct Home: View { + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + Container { + H1("Welcome to Coco's Website") + .fontSize(.extraExtraExtraLarge) + .bold() + Text("Learn all about Coco the swimming pig!") + } + .padding(.vertical, 48) + } + } + } +} + +let sitemap: Sitemap = [ + "index.html": Home() +] + +guard let projectURL = URL(filePath: #filePath)? + .deletingLastPathComponent() + .deletingLastPathComponent() else { + print("Unable to create URL for \(#filePath)") + exit(1) +} + +let outputURL = projectURL.appending(path: "site") + +try renderSitemap(sitemap, to: outputURL) diff --git a/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/MultiPageSite/IntroducingSlipstream-YourFirstSite-MultiPageSite-1-2.swift b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/MultiPageSite/IntroducingSlipstream-YourFirstSite-MultiPageSite-1-2.swift new file mode 100644 index 00000000..6cdd7cbe --- /dev/null +++ b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/MultiPageSite/IntroducingSlipstream-YourFirstSite-MultiPageSite-1-2.swift @@ -0,0 +1,57 @@ +import Foundation + +import Slipstream + +struct Home: View { + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + Container { + H1("Welcome to Coco's Website") + .fontSize(.extraExtraExtraLarge) + .bold() + Text("Learn all about Coco the swimming pig!") + } + .padding(.vertical, 48) + } + } + } +} + +struct About: View { + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + Container { + H1("About Coco") + .fontSize(.extraExtraExtraLarge) + .bold() + Text("Coco is a swimming pig who lives in the beautiful Bahamas. She loves to swim in the crystal-clear waters and greet tourists who visit the beach.") + Text("Swimming pigs have become one of the most popular attractions in the Bahamas, and Coco is one of the friendliest!") + } + .padding(.vertical, 48) + } + } + } +} + +let sitemap: Sitemap = [ + "index.html": Home() +] + +guard let projectURL = URL(filePath: #filePath)? + .deletingLastPathComponent() + .deletingLastPathComponent() else { + print("Unable to create URL for \(#filePath)") + exit(1) +} + +let outputURL = projectURL.appending(path: "site") + +try renderSitemap(sitemap, to: outputURL) diff --git a/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/MultiPageSite/IntroducingSlipstream-YourFirstSite-MultiPageSite-1-3.swift b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/MultiPageSite/IntroducingSlipstream-YourFirstSite-MultiPageSite-1-3.swift new file mode 100644 index 00000000..ef4759d1 --- /dev/null +++ b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/MultiPageSite/IntroducingSlipstream-YourFirstSite-MultiPageSite-1-3.swift @@ -0,0 +1,89 @@ +import Foundation + +import Slipstream + +struct Home: View { + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + Container { + H1("Welcome to Coco's Website") + .fontSize(.extraExtraExtraLarge) + .bold() + Text("Learn all about Coco the swimming pig!") + } + .padding(.vertical, 48) + } + } + } +} + +struct About: View { + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + Container { + H1("About Coco") + .fontSize(.extraExtraExtraLarge) + .bold() + Text("Coco is a swimming pig who lives in the beautiful Bahamas. She loves to swim in the crystal-clear waters and greet tourists who visit the beach.") + Text("Swimming pigs have become one of the most popular attractions in the Bahamas, and Coco is one of the friendliest!") + } + .padding(.vertical, 48) + } + } + } +} + +struct Gallery: View { + let photos = ["coco-swimming.jpg", "coco-beach.jpg", "coco-friends.jpg", "coco-sunset.jpg"] + + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + Container { + H1("Photo Gallery") + .fontSize(.extraExtraExtraLarge) + .bold() + .margin(.bottom, 32) + + Div { + ForEach(photos, id: \.self) { photo in + Image(URL(string: photo)) + .cornerRadius(.large) + } + } + .display(.grid) + .gridTemplateColumns(2) + .gridTemplateColumns(3, condition: .startingAt(.medium)) + .flexGap(16) + } + .padding(.vertical, 48) + } + } + } +} + +let sitemap: Sitemap = [ + "index.html": Home() +] + +guard let projectURL = URL(filePath: #filePath)? + .deletingLastPathComponent() + .deletingLastPathComponent() else { + print("Unable to create URL for \(#filePath)") + exit(1) +} + +let outputURL = projectURL.appending(path: "site") + +try renderSitemap(sitemap, to: outputURL) diff --git a/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/MultiPageSite/IntroducingSlipstream-YourFirstSite-MultiPageSite-1-4.swift b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/MultiPageSite/IntroducingSlipstream-YourFirstSite-MultiPageSite-1-4.swift new file mode 100644 index 00000000..34ac920e --- /dev/null +++ b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/MultiPageSite/IntroducingSlipstream-YourFirstSite-MultiPageSite-1-4.swift @@ -0,0 +1,91 @@ +import Foundation + +import Slipstream + +struct Home: View { + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + Container { + H1("Welcome to Coco's Website") + .fontSize(.extraExtraExtraLarge) + .bold() + Text("Learn all about Coco the swimming pig!") + } + .padding(.vertical, 48) + } + } + } +} + +struct About: View { + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + Container { + H1("About Coco") + .fontSize(.extraExtraExtraLarge) + .bold() + Text("Coco is a swimming pig who lives in the beautiful Bahamas. She loves to swim in the crystal-clear waters and greet tourists who visit the beach.") + Text("Swimming pigs have become one of the most popular attractions in the Bahamas, and Coco is one of the friendliest!") + } + .padding(.vertical, 48) + } + } + } +} + +struct Gallery: View { + let photos = ["coco-swimming.jpg", "coco-beach.jpg", "coco-friends.jpg", "coco-sunset.jpg"] + + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + Container { + H1("Photo Gallery") + .fontSize(.extraExtraExtraLarge) + .bold() + .margin(.bottom, 32) + + Div { + ForEach(photos, id: \.self) { photo in + Image(URL(string: photo)) + .cornerRadius(.large) + } + } + .display(.grid) + .gridTemplateColumns(2) + .gridTemplateColumns(3, condition: .startingAt(.medium)) + .flexGap(16) + } + .padding(.vertical, 48) + } + } + } +} + +let sitemap: Sitemap = [ + "index.html": Home(), + "about.html": About(), + "gallery.html": Gallery() +] + +guard let projectURL = URL(filePath: #filePath)? + .deletingLastPathComponent() + .deletingLastPathComponent() else { + print("Unable to create URL for \(#filePath)") + exit(1) +} + +let outputURL = projectURL.appending(path: "site") + +try renderSitemap(sitemap, to: outputURL) diff --git a/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/MultiPageSite/IntroducingSlipstream-YourFirstSite-MultiPageSite-2-1.swift b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/MultiPageSite/IntroducingSlipstream-YourFirstSite-MultiPageSite-2-1.swift new file mode 100644 index 00000000..87e57278 --- /dev/null +++ b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/MultiPageSite/IntroducingSlipstream-YourFirstSite-MultiPageSite-2-1.swift @@ -0,0 +1,108 @@ +import Foundation + +import Slipstream + +struct NavigationBar: View { + var body: some View { + Navigation { + Container { + HStack(alignment: .center, spacing: 24) { + Link("Home", destination: URL(string: "index.html")) + Link("About", destination: URL(string: "about.html")) + Link("Gallery", destination: URL(string: "gallery.html")) + } + .padding(.vertical, 16) + } + } + .background(.gray, darkness: 100) + .border(.bottom, width: 1, color: .gray, darkness: 200) + } +} + +struct Home: View { + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + Container { + H1("Welcome to Coco's Website") + .fontSize(.extraExtraExtraLarge) + .bold() + Text("Learn all about Coco the swimming pig!") + } + .padding(.vertical, 48) + } + } + } +} + +struct About: View { + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + Container { + H1("About Coco") + .fontSize(.extraExtraExtraLarge) + .bold() + Text("Coco is a swimming pig who lives in the beautiful Bahamas. She loves to swim in the crystal-clear waters and greet tourists who visit the beach.") + Text("Swimming pigs have become one of the most popular attractions in the Bahamas, and Coco is one of the friendliest!") + } + .padding(.vertical, 48) + } + } + } +} + +struct Gallery: View { + let photos = ["coco-swimming.jpg", "coco-beach.jpg", "coco-friends.jpg", "coco-sunset.jpg"] + + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + Container { + H1("Photo Gallery") + .fontSize(.extraExtraExtraLarge) + .bold() + .margin(.bottom, 32) + + Div { + ForEach(photos, id: \.self) { photo in + Image(URL(string: photo)) + .cornerRadius(.large) + } + } + .display(.grid) + .gridTemplateColumns(2) + .gridTemplateColumns(3, condition: .startingAt(.medium)) + .flexGap(16) + } + .padding(.vertical, 48) + } + } + } +} + +let sitemap: Sitemap = [ + "index.html": Home(), + "about.html": About(), + "gallery.html": Gallery() +] + +guard let projectURL = URL(filePath: #filePath)? + .deletingLastPathComponent() + .deletingLastPathComponent() else { + print("Unable to create URL for \(#filePath)") + exit(1) +} + +let outputURL = projectURL.appending(path: "site") + +try renderSitemap(sitemap, to: outputURL) diff --git a/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/MultiPageSite/IntroducingSlipstream-YourFirstSite-MultiPageSite-2-2.swift b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/MultiPageSite/IntroducingSlipstream-YourFirstSite-MultiPageSite-2-2.swift new file mode 100644 index 00000000..8ca24080 --- /dev/null +++ b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/MultiPageSite/IntroducingSlipstream-YourFirstSite-MultiPageSite-2-2.swift @@ -0,0 +1,111 @@ +import Foundation + +import Slipstream + +struct NavigationBar: View { + var body: some View { + Navigation { + Container { + HStack(alignment: .center, spacing: 24) { + Link("Home", destination: URL(string: "index.html")) + Link("About", destination: URL(string: "about.html")) + Link("Gallery", destination: URL(string: "gallery.html")) + } + .padding(.vertical, 16) + } + } + .background(.gray, darkness: 100) + .border(.bottom, width: 1, color: .gray, darkness: 200) + } +} + +struct Home: View { + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + NavigationBar() + Container { + H1("Welcome to Coco's Website") + .fontSize(.extraExtraExtraLarge) + .bold() + Text("Learn all about Coco the swimming pig!") + } + .padding(.vertical, 48) + } + } + } +} + +struct About: View { + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + NavigationBar() + Container { + H1("About Coco") + .fontSize(.extraExtraExtraLarge) + .bold() + Text("Coco is a swimming pig who lives in the beautiful Bahamas. She loves to swim in the crystal-clear waters and greet tourists who visit the beach.") + Text("Swimming pigs have become one of the most popular attractions in the Bahamas, and Coco is one of the friendliest!") + } + .padding(.vertical, 48) + } + } + } +} + +struct Gallery: View { + let photos = ["coco-swimming.jpg", "coco-beach.jpg", "coco-friends.jpg", "coco-sunset.jpg"] + + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + NavigationBar() + Container { + H1("Photo Gallery") + .fontSize(.extraExtraExtraLarge) + .bold() + .margin(.bottom, 32) + + Div { + ForEach(photos, id: \.self) { photo in + Image(URL(string: photo)) + .cornerRadius(.large) + } + } + .display(.grid) + .gridTemplateColumns(2) + .gridTemplateColumns(3, condition: .startingAt(.medium)) + .flexGap(16) + } + .padding(.vertical, 48) + } + } + } +} + +let sitemap: Sitemap = [ + "index.html": Home(), + "about.html": About(), + "gallery.html": Gallery() +] + +guard let projectURL = URL(filePath: #filePath)? + .deletingLastPathComponent() + .deletingLastPathComponent() else { + print("Unable to create URL for \(#filePath)") + exit(1) +} + +let outputURL = projectURL.appending(path: "site") + +try renderSitemap(sitemap, to: outputURL) diff --git a/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/MultiPageSite/IntroducingSlipstream-YourFirstSite-MultiPageSite-3-1.swift b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/MultiPageSite/IntroducingSlipstream-YourFirstSite-MultiPageSite-3-1.swift new file mode 100644 index 00000000..e266ed62 --- /dev/null +++ b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/MultiPageSite/IntroducingSlipstream-YourFirstSite-MultiPageSite-3-1.swift @@ -0,0 +1,130 @@ +import Foundation + +import Slipstream + +struct NavigationBar: View { + var body: some View { + Navigation { + Container { + HStack(alignment: .center, spacing: 24) { + Link("Home", destination: URL(string: "index.html")) + Link("About", destination: URL(string: "about.html")) + Link("Gallery", destination: URL(string: "gallery.html")) + } + .padding(.vertical, 16) + } + } + .background(.gray, darkness: 100) + .border(.bottom, width: 1, color: .gray, darkness: 200) + } +} + +struct PageLayout: View { + @ViewBuilder let content: () -> Content + + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + NavigationBar() + Container { + content() + } + .padding(.vertical, 48) + } + } + } +} + +struct Home: View { + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + NavigationBar() + Container { + H1("Welcome to Coco's Website") + .fontSize(.extraExtraExtraLarge) + .bold() + Text("Learn all about Coco the swimming pig!") + } + .padding(.vertical, 48) + } + } + } +} + +struct About: View { + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + NavigationBar() + Container { + H1("About Coco") + .fontSize(.extraExtraExtraLarge) + .bold() + Text("Coco is a swimming pig who lives in the beautiful Bahamas. She loves to swim in the crystal-clear waters and greet tourists who visit the beach.") + Text("Swimming pigs have become one of the most popular attractions in the Bahamas, and Coco is one of the friendliest!") + } + .padding(.vertical, 48) + } + } + } +} + +struct Gallery: View { + let photos = ["coco-swimming.jpg", "coco-beach.jpg", "coco-friends.jpg", "coco-sunset.jpg"] + + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + NavigationBar() + Container { + H1("Photo Gallery") + .fontSize(.extraExtraExtraLarge) + .bold() + .margin(.bottom, 32) + + Div { + ForEach(photos, id: \.self) { photo in + Image(URL(string: photo)) + .cornerRadius(.large) + } + } + .display(.grid) + .gridTemplateColumns(2) + .gridTemplateColumns(3, condition: .startingAt(.medium)) + .flexGap(16) + } + .padding(.vertical, 48) + } + } + } +} + +let sitemap: Sitemap = [ + "index.html": Home(), + "about.html": About(), + "gallery.html": Gallery() +] + +guard let projectURL = URL(filePath: #filePath)? + .deletingLastPathComponent() + .deletingLastPathComponent() else { + print("Unable to create URL for \(#filePath)") + exit(1) +} + +let outputURL = projectURL.appending(path: "site") + +try renderSitemap(sitemap, to: outputURL) diff --git a/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/MultiPageSite/IntroducingSlipstream-YourFirstSite-MultiPageSite-3-2.swift b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/MultiPageSite/IntroducingSlipstream-YourFirstSite-MultiPageSite-3-2.swift new file mode 100644 index 00000000..fef4b8c2 --- /dev/null +++ b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/MultiPageSite/IntroducingSlipstream-YourFirstSite-MultiPageSite-3-2.swift @@ -0,0 +1,103 @@ +import Foundation + +import Slipstream + +struct NavigationBar: View { + var body: some View { + Navigation { + Container { + HStack(alignment: .center, spacing: 24) { + Link("Home", destination: URL(string: "index.html")) + Link("About", destination: URL(string: "about.html")) + Link("Gallery", destination: URL(string: "gallery.html")) + } + .padding(.vertical, 16) + } + } + .background(.gray, darkness: 100) + .border(.bottom, width: 1, color: .gray, darkness: 200) + } +} + +struct PageLayout: View { + @ViewBuilder let content: () -> Content + + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + NavigationBar() + Container { + content() + } + .padding(.vertical, 48) + } + } + } +} + +struct Home: View { + var body: some View { + PageLayout { + H1("Welcome to Coco's Website") + .fontSize(.extraExtraExtraLarge) + .bold() + Text("Learn all about Coco the swimming pig!") + } + } +} + +struct About: View { + var body: some View { + PageLayout { + H1("About Coco") + .fontSize(.extraExtraExtraLarge) + .bold() + Text("Coco is a swimming pig who lives in the beautiful Bahamas. She loves to swim in the crystal-clear waters and greet tourists who visit the beach.") + Text("Swimming pigs have become one of the most popular attractions in the Bahamas, and Coco is one of the friendliest!") + } + } +} + +struct Gallery: View { + let photos = ["coco-swimming.jpg", "coco-beach.jpg", "coco-friends.jpg", "coco-sunset.jpg"] + + var body: some View { + PageLayout { + H1("Photo Gallery") + .fontSize(.extraExtraExtraLarge) + .bold() + .margin(.bottom, 32) + + Div { + ForEach(photos, id: \.self) { photo in + Image(URL(string: photo)) + .cornerRadius(.large) + } + } + .display(.grid) + .gridTemplateColumns(2) + .gridTemplateColumns(3, condition: .startingAt(.medium)) + .flexGap(16) + } + } +} + +let sitemap: Sitemap = [ + "index.html": Home(), + "about.html": About(), + "gallery.html": Gallery() +] + +guard let projectURL = URL(filePath: #filePath)? + .deletingLastPathComponent() + .deletingLastPathComponent() else { + print("Unable to create URL for \(#filePath)") + exit(1) +} + +let outputURL = projectURL.appending(path: "site") + +try renderSitemap(sitemap, to: outputURL) diff --git a/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/MultiPageSite/IntroducingSlipstream-YourFirstSite-MultiPageSite-3-3.swift b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/MultiPageSite/IntroducingSlipstream-YourFirstSite-MultiPageSite-3-3.swift new file mode 100644 index 00000000..9feb600b --- /dev/null +++ b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/MultiPageSite/IntroducingSlipstream-YourFirstSite-MultiPageSite-3-3.swift @@ -0,0 +1,130 @@ +import Foundation + +import Slipstream + +struct NavigationBar: View { + var body: some View { + Navigation { + Container { + HStack(alignment: .center, spacing: 24) { + Link("Home", destination: URL(string: "index.html")) + .textColor(.gray, darkness: 700) + .textColor(.blue, darkness: 600, condition: .hover) + Link("About", destination: URL(string: "about.html")) + .textColor(.gray, darkness: 700) + .textColor(.blue, darkness: 600, condition: .hover) + Link("Gallery", destination: URL(string: "gallery.html")) + .textColor(.gray, darkness: 700) + .textColor(.blue, darkness: 600, condition: .hover) + } + .padding(.vertical, 16) + } + } + .background(.white) + .border(.bottom, width: 1, color: .gray, darkness: 200) + .shadow(.small) + } +} + +struct PageFooter: View { + var body: some View { + Footer { + Container { + Div { + Text("© 2024 Coco's Website. All rights reserved.") + .textColor(.gray, darkness: 600) + .fontSize(.small) + } + .padding(.vertical, 24) + .textAlignment(.center) + } + } + .background(.gray, darkness: 50) + .border(.top, width: 1, color: .gray, darkness: 200) + .margin(.top, 64) + } +} + +struct PageLayout: View { + @ViewBuilder let content: () -> Content + + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + NavigationBar() + Container { + content() + } + .padding(.vertical, 48) + PageFooter() + } + } + } +} + +struct Home: View { + var body: some View { + PageLayout { + H1("Welcome to Coco's Website") + .fontSize(.extraExtraExtraLarge) + .bold() + Text("Learn all about Coco the swimming pig!") + } + } +} + +struct About: View { + var body: some View { + PageLayout { + H1("About Coco") + .fontSize(.extraExtraExtraLarge) + .bold() + Text("Coco is a swimming pig who lives in the beautiful Bahamas. She loves to swim in the crystal-clear waters and greet tourists who visit the beach.") + Text("Swimming pigs have become one of the most popular attractions in the Bahamas, and Coco is one of the friendliest!") + } + } +} + +struct Gallery: View { + let photos = ["coco-swimming.jpg", "coco-beach.jpg", "coco-friends.jpg", "coco-sunset.jpg"] + + var body: some View { + PageLayout { + H1("Photo Gallery") + .fontSize(.extraExtraExtraLarge) + .bold() + .margin(.bottom, 32) + + Div { + ForEach(photos, id: \.self) { photo in + Image(URL(string: photo)) + .cornerRadius(.large) + } + } + .display(.grid) + .gridTemplateColumns(2) + .gridTemplateColumns(3, condition: .startingAt(.medium)) + .flexGap(16) + } + } +} + +let sitemap: Sitemap = [ + "index.html": Home(), + "about.html": About(), + "gallery.html": Gallery() +] + +guard let projectURL = URL(filePath: #filePath)? + .deletingLastPathComponent() + .deletingLastPathComponent() else { + print("Unable to create URL for \(#filePath)") + exit(1) +} + +let outputURL = projectURL.appending(path: "site") + +try renderSitemap(sitemap, to: outputURL) diff --git a/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/MultiPageSite/IntroducingSlipstream-YourFirstSite-MultiPageSite-4-1.swift b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/MultiPageSite/IntroducingSlipstream-YourFirstSite-MultiPageSite-4-1.swift new file mode 100644 index 00000000..e4b46e24 --- /dev/null +++ b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/MultiPageSite/IntroducingSlipstream-YourFirstSite-MultiPageSite-4-1.swift @@ -0,0 +1,133 @@ +import Foundation + +import Slipstream + +struct NavigationBar: View { + let activePage: String + + var body: some View { + Navigation { + Container { + HStack(alignment: .center, spacing: 24) { + Link("Home", destination: URL(string: "index.html")) + .textColor(.gray, darkness: 700) + .textColor(.blue, darkness: 600, condition: .hover) + Link("About", destination: URL(string: "about.html")) + .textColor(.gray, darkness: 700) + .textColor(.blue, darkness: 600, condition: .hover) + Link("Gallery", destination: URL(string: "gallery.html")) + .textColor(.gray, darkness: 700) + .textColor(.blue, darkness: 600, condition: .hover) + } + .padding(.vertical, 16) + } + } + .background(.white) + .border(.bottom, width: 1, color: .gray, darkness: 200) + .shadow(.small) + } +} + +struct PageFooter: View { + var body: some View { + Footer { + Container { + Div { + Text("© 2024 Coco's Website. All rights reserved.") + .textColor(.gray, darkness: 600) + .fontSize(.small) + } + .padding(.vertical, 24) + .textAlignment(.center) + } + } + .background(.gray, darkness: 50) + .border(.top, width: 1, color: .gray, darkness: 200) + .margin(.top, 64) + } +} + +struct PageLayout: View { + let activePage: String + @ViewBuilder let content: () -> Content + + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + NavigationBar(activePage: activePage) + Container { + content() + } + .padding(.vertical, 48) + PageFooter() + } + } + } +} + +struct Home: View { + var body: some View { + PageLayout(activePage: "home") { + H1("Welcome to Coco's Website") + .fontSize(.extraExtraExtraLarge) + .bold() + Text("Learn all about Coco the swimming pig!") + } + } +} + +struct About: View { + var body: some View { + PageLayout(activePage: "about") { + H1("About Coco") + .fontSize(.extraExtraExtraLarge) + .bold() + Text("Coco is a swimming pig who lives in the beautiful Bahamas. She loves to swim in the crystal-clear waters and greet tourists who visit the beach.") + Text("Swimming pigs have become one of the most popular attractions in the Bahamas, and Coco is one of the friendliest!") + } + } +} + +struct Gallery: View { + let photos = ["coco-swimming.jpg", "coco-beach.jpg", "coco-friends.jpg", "coco-sunset.jpg"] + + var body: some View { + PageLayout(activePage: "gallery") { + H1("Photo Gallery") + .fontSize(.extraExtraExtraLarge) + .bold() + .margin(.bottom, 32) + + Div { + ForEach(photos, id: \.self) { photo in + Image(URL(string: photo)) + .cornerRadius(.large) + } + } + .display(.grid) + .gridTemplateColumns(2) + .gridTemplateColumns(3, condition: .startingAt(.medium)) + .flexGap(16) + } + } +} + +let sitemap: Sitemap = [ + "index.html": Home(), + "about.html": About(), + "gallery.html": Gallery() +] + +guard let projectURL = URL(filePath: #filePath)? + .deletingLastPathComponent() + .deletingLastPathComponent() else { + print("Unable to create URL for \(#filePath)") + exit(1) +} + +let outputURL = projectURL.appending(path: "site") + +try renderSitemap(sitemap, to: outputURL) diff --git a/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/MultiPageSite/IntroducingSlipstream-YourFirstSite-MultiPageSite-4-2.swift b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/MultiPageSite/IntroducingSlipstream-YourFirstSite-MultiPageSite-4-2.swift new file mode 100644 index 00000000..779a3416 --- /dev/null +++ b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/MultiPageSite/IntroducingSlipstream-YourFirstSite-MultiPageSite-4-2.swift @@ -0,0 +1,136 @@ +import Foundation + +import Slipstream + +struct NavigationBar: View { + let activePage: String + + @ViewBuilder + private func navLink(_ title: String, page: String, url: String) -> some View { + let isActive = activePage == page + Link(title, destination: URL(string: url)) + .textColor(isActive ? .blue : .gray, darkness: isActive ? 600 : 700) + .bold(isActive) + .textColor(.blue, darkness: 600, condition: .hover) + } + + var body: some View { + Navigation { + Container { + HStack(alignment: .center, spacing: 24) { + navLink("Home", page: "home", url: "index.html") + navLink("About", page: "about", url: "about.html") + navLink("Gallery", page: "gallery", url: "gallery.html") + } + .padding(.vertical, 16) + } + } + .background(.white) + .border(.bottom, width: 1, color: .gray, darkness: 200) + .shadow(.small) + } +} + +struct PageFooter: View { + var body: some View { + Footer { + Container { + Div { + Text("© 2024 Coco's Website. All rights reserved.") + .textColor(.gray, darkness: 600) + .fontSize(.small) + } + .padding(.vertical, 24) + .textAlignment(.center) + } + } + .background(.gray, darkness: 50) + .border(.top, width: 1, color: .gray, darkness: 200) + .margin(.top, 64) + } +} + +struct PageLayout: View { + let activePage: String + @ViewBuilder let content: () -> Content + + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + NavigationBar(activePage: activePage) + Container { + content() + } + .padding(.vertical, 48) + PageFooter() + } + } + } +} + +struct Home: View { + var body: some View { + PageLayout(activePage: "home") { + H1("Welcome to Coco's Website") + .fontSize(.extraExtraExtraLarge) + .bold() + Text("Learn all about Coco the swimming pig!") + } + } +} + +struct About: View { + var body: some View { + PageLayout(activePage: "about") { + H1("About Coco") + .fontSize(.extraExtraExtraLarge) + .bold() + Text("Coco is a swimming pig who lives in the beautiful Bahamas. She loves to swim in the crystal-clear waters and greet tourists who visit the beach.") + Text("Swimming pigs have become one of the most popular attractions in the Bahamas, and Coco is one of the friendliest!") + } + } +} + +struct Gallery: View { + let photos = ["coco-swimming.jpg", "coco-beach.jpg", "coco-friends.jpg", "coco-sunset.jpg"] + + var body: some View { + PageLayout(activePage: "gallery") { + H1("Photo Gallery") + .fontSize(.extraExtraExtraLarge) + .bold() + .margin(.bottom, 32) + + Div { + ForEach(photos, id: \.self) { photo in + Image(URL(string: photo)) + .cornerRadius(.large) + } + } + .display(.grid) + .gridTemplateColumns(2) + .gridTemplateColumns(3, condition: .startingAt(.medium)) + .flexGap(16) + } + } +} + +let sitemap: Sitemap = [ + "index.html": Home(), + "about.html": About(), + "gallery.html": Gallery() +] + +guard let projectURL = URL(filePath: #filePath)? + .deletingLastPathComponent() + .deletingLastPathComponent() else { + print("Unable to create URL for \(#filePath)") + exit(1) +} + +let outputURL = projectURL.appending(path: "site") + +try renderSitemap(sitemap, to: outputURL) diff --git a/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/MultiPageSite/IntroducingSlipstream-YourFirstSite-MultiPageSite-4-3.swift b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/MultiPageSite/IntroducingSlipstream-YourFirstSite-MultiPageSite-4-3.swift new file mode 100644 index 00000000..779a3416 --- /dev/null +++ b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/MultiPageSite/IntroducingSlipstream-YourFirstSite-MultiPageSite-4-3.swift @@ -0,0 +1,136 @@ +import Foundation + +import Slipstream + +struct NavigationBar: View { + let activePage: String + + @ViewBuilder + private func navLink(_ title: String, page: String, url: String) -> some View { + let isActive = activePage == page + Link(title, destination: URL(string: url)) + .textColor(isActive ? .blue : .gray, darkness: isActive ? 600 : 700) + .bold(isActive) + .textColor(.blue, darkness: 600, condition: .hover) + } + + var body: some View { + Navigation { + Container { + HStack(alignment: .center, spacing: 24) { + navLink("Home", page: "home", url: "index.html") + navLink("About", page: "about", url: "about.html") + navLink("Gallery", page: "gallery", url: "gallery.html") + } + .padding(.vertical, 16) + } + } + .background(.white) + .border(.bottom, width: 1, color: .gray, darkness: 200) + .shadow(.small) + } +} + +struct PageFooter: View { + var body: some View { + Footer { + Container { + Div { + Text("© 2024 Coco's Website. All rights reserved.") + .textColor(.gray, darkness: 600) + .fontSize(.small) + } + .padding(.vertical, 24) + .textAlignment(.center) + } + } + .background(.gray, darkness: 50) + .border(.top, width: 1, color: .gray, darkness: 200) + .margin(.top, 64) + } +} + +struct PageLayout: View { + let activePage: String + @ViewBuilder let content: () -> Content + + var body: some View { + HTML { + Head { + Stylesheet(URL(string: "main.css")) + } + Body { + NavigationBar(activePage: activePage) + Container { + content() + } + .padding(.vertical, 48) + PageFooter() + } + } + } +} + +struct Home: View { + var body: some View { + PageLayout(activePage: "home") { + H1("Welcome to Coco's Website") + .fontSize(.extraExtraExtraLarge) + .bold() + Text("Learn all about Coco the swimming pig!") + } + } +} + +struct About: View { + var body: some View { + PageLayout(activePage: "about") { + H1("About Coco") + .fontSize(.extraExtraExtraLarge) + .bold() + Text("Coco is a swimming pig who lives in the beautiful Bahamas. She loves to swim in the crystal-clear waters and greet tourists who visit the beach.") + Text("Swimming pigs have become one of the most popular attractions in the Bahamas, and Coco is one of the friendliest!") + } + } +} + +struct Gallery: View { + let photos = ["coco-swimming.jpg", "coco-beach.jpg", "coco-friends.jpg", "coco-sunset.jpg"] + + var body: some View { + PageLayout(activePage: "gallery") { + H1("Photo Gallery") + .fontSize(.extraExtraExtraLarge) + .bold() + .margin(.bottom, 32) + + Div { + ForEach(photos, id: \.self) { photo in + Image(URL(string: photo)) + .cornerRadius(.large) + } + } + .display(.grid) + .gridTemplateColumns(2) + .gridTemplateColumns(3, condition: .startingAt(.medium)) + .flexGap(16) + } + } +} + +let sitemap: Sitemap = [ + "index.html": Home(), + "about.html": About(), + "gallery.html": Gallery() +] + +guard let projectURL = URL(filePath: #filePath)? + .deletingLastPathComponent() + .deletingLastPathComponent() else { + print("Unable to create URL for \(#filePath)") + exit(1) +} + +let outputURL = projectURL.appending(path: "site") + +try renderSitemap(sitemap, to: outputURL) diff --git a/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/MultiPageSite/IntroducingSlipstream-YourFirstSite-MultiPageSite.tutorial b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/MultiPageSite/IntroducingSlipstream-YourFirstSite-MultiPageSite.tutorial new file mode 100644 index 00000000..22fb12d2 --- /dev/null +++ b/Sources/Slipstream/Documentation.docc/Tutorials/IntroducingSlipstream/YourFirstSite/MultiPageSite/IntroducingSlipstream-YourFirstSite-MultiPageSite.tutorial @@ -0,0 +1,174 @@ +@Tutorial(time: 20) { + @XcodeRequirement(title: "Xcode 15 or later", destination: "https://itunes.apple.com/us/app/xcode/id497799835?mt=12") + + @Intro(title: "Building a Multi-page Site") { + Learn how to create a website with multiple pages, shared layouts, and navigation menus. + + This tutorial will show you how to build a complete website with a home page, about page, and gallery page, all connected through a navigation menu. + + @Image(source: "tutorial-banner", alt: "Coco the pig") + } + + @Section(title: "Creating multiple pages") { + @ContentAndMedia { + A website typically consists of multiple pages. In Slipstream, you define pages by adding them to your ``Sitemap``. + + Let's create a simple website with three pages: Home, About, and Gallery. + + @Image(source: "logo-square", alt: "The Slipstream logo") + } + + @Steps { + @Step { + Start with a basic home page. We'll expand on this as we go. + + @Code(name: "main.swift", file: "IntroducingSlipstream-YourFirstSite-MultiPageSite-1-1.swift") + } + + @Step { + Create an About page view. This will be a separate page that tells visitors more about Coco. + + @Code(name: "main.swift", file: "IntroducingSlipstream-YourFirstSite-MultiPageSite-1-2.swift") + } + + @Step { + Create a Gallery page that will display Coco's photos. + + @Code(name: "main.swift", file: "IntroducingSlipstream-YourFirstSite-MultiPageSite-1-3.swift") + } + + @Step { + Now add all three pages to your sitemap. The sitemap is a dictionary that maps file paths to views. + + Notice that we're creating `about.html` and `gallery.html` files in addition to `index.html`. + + @Code(name: "main.swift", file: "IntroducingSlipstream-YourFirstSite-MultiPageSite-1-4.swift") { + @Image(source: "IntroducingSlipstream-YourFirstSite-MultiPageSite-1-4-preview", alt: "Multiple HTML files generated") + } + } + + @Step { + Run your executable to generate all three pages. You should now have three HTML files in your `site/` directory. + + You can test navigation by manually opening each file in your browser. + + @Image(source: "IntroducingSlipstream-YourFirstSite-MultiPageSite-1-5", alt: "Three separate pages") + } + } + } + + @Section(title: "Adding a navigation menu") { + @ContentAndMedia { + Now that we have multiple pages, we need a way to navigate between them. Let's create a navigation menu that appears on all pages. + + @Image(source: "logo-square", alt: "The Slipstream logo") + } + + @Steps { + @Step { + Create a navigation component using the ``Navigation`` view. This creates a semantic `