Rust library to generate static websites
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(website): some sort of mobile theme

+242 -59
+1
website/assets/close.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M4.3 4.3a1 1 0 0 1 1.4 0L10 8.58l4.3-4.3a1 1 0 1 1 1.4 1.42L11.42 10l4.3 4.3a1 1 0 0 1-1.42 1.4L10 11.42l-4.3 4.3a1 1 0 0 1-1.4-1.42L8.58 10l-4.3-4.3a1 1 0 0 1 0-1.4z" clip-rule="evenodd"/></svg>
+1
website/assets/hamburger.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M2 4a1 1 0 0 1 1-1h18a1 1 0 0 1 0 2H3a1 1 0 0 1-1-1Zm1 9h18a1 1 0 0 0 0-2H3a1 1 0 0 0 0 2Zm0 8h18a1 1 0 0 0 0-2H3a1 1 0 0 0 0 2Z"/></svg>
+2
website/assets/prin.css
··· 1 1 @import "tailwindcss"; 2 + 3 + /* TODO: Get rid of this and build our own `prose` class */ 2 4 @plugin "@tailwindcss/typography"; 3 5 4 6 @theme {
+1
website/assets/side-menu.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor"><path d="M3 5a1 1 0 0 1 1-1h12a1 1 0 1 1 0 2H4a1 1 0 0 1-1-1zm0 5a1 1 0 0 1 1-1h12a1 1 0 1 1 0 2H4a1 1 0 0 1-1-1zm0 5a1 1 0 0 1 1-1h12a1 1 0 1 1 0 2H4a1 1 0 0 1-1-1z"/></svg>
+3
website/assets/toc.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"> 2 + <path fill="currentColor" transform="translate(0,1)" d="M4 9h12l1-1-1-1H4L3 8l1 1m0 4h12l1-1-1-1H4l-1 1 1 1m0 4h12l1-1-1-1H4l-1 1 1 1m15 0h2v-2h-2zm0-10v2h2V7zm0 6h2v-2h-2z"/> 3 + </svg>
+9 -13
website/content/docs/javascript.md
··· 6 6 7 7 JavaScript and TypeScript files can be added to pages using the `ctx.assets.add_script()` method. 8 8 9 + In [supported templating languages](/docs/templating/), the return value of `ctx.assets.add_script()` can be used directly in the template. 10 + 9 11 ```rs 10 12 use maudit::route::prelude::*; 11 - use maud::{html, Markup}; 13 + use maud::{html}; 12 14 13 - #[route("/blog")] 14 - pub struct Blog; 15 + #[route("/")] 16 + pub struct Index; 15 17 16 - impl Route<PageParams, Markup> for Blog { 17 - fn render(&self, ctx: &mut PageContext) -> Markup { 18 + impl Route for Index { 19 + fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> { 18 20 let script = ctx.assets.add_script("script.js"); 19 21 20 22 html! { ··· 27 29 The `include_script()` method can be used to automatically include the script in the page, which can be useful when using layouts or other shared templates. 28 30 29 31 ```rs 30 - fn render(&self, ctx: &mut PageContext) -> Markup { 32 + fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> { 31 33 ctx.assets.include_script("script.js"); 32 34 33 - layout( 34 - html! { 35 - div { 36 - "Look ma, no explicit script tag!" 37 - } 38 - } 39 - ) 35 + layout("Look ma, no explicit script tag!") 40 36 } 41 37 ``` 42 38
+7 -13
website/content/docs/styling.md
··· 14 14 use maudit::route::prelude::*; 15 15 use maud::{html, Markup}; 16 16 17 - #[route("/blog")] 18 - pub struct Blog; 17 + #[route("/")] 18 + pub struct Index; 19 19 20 - impl Route<PageParams, Markup> for Blog { 21 - fn render(&self, ctx: &mut PageContext) -> Markup { 20 + impl Route for Blog { 21 + fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> { 22 22 let style = ctx.assets.add_style("style.css"); 23 23 24 24 html! { ··· 31 31 Alternatively, the `include_style()` method can be used to automatically include the stylesheet in the page, without needing to manually add it to the template. Note that, at this time, pages without a `head` tag won't have the stylesheet included. 32 32 33 33 ```rs 34 - fn render(&self, ctx: &mut PageContext) -> Markup { 34 + fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> { 35 35 ctx.assets.include_style("style.css"); 36 36 37 - layout( 38 - html! { 39 - div { 40 - "Look ma, no link tag!" 41 - } 42 - } 43 - ) 37 + layout("Look ma, no link tag!") 44 38 } 45 39 ``` 46 40 ··· 49 43 Maudit includes built-in support for [Tailwind CSS](https://tailwindcss.com/). To use it, use `add_style_with_options()` or `include_style_with_options()` with the `StyleOptions { tailwind: true }` option. 50 44 51 45 ```rs 52 - fn render(&self, ctx: &mut PageContext) -> Markup { 46 + fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> { 53 47 ctx.assets.add_style_with_options("style.css", StyleOptions { tailwind: true }); 54 48 55 49 html! {
+4 -4
website/content/docs/templating.md
··· 19 19 #[route("/")] 20 20 pub struct Index; 21 21 22 - impl Route<PageParams, Markup> for Index { 23 - fn render(&self, _: &mut PageContext) -> Markup { 22 + impl Route for Index { 23 + fn render(&self, _: &mut PageContext) -> impl Into<RenderResult> { 24 24 html! { 25 25 h1 { "Hello, world!" } 26 26 } ··· 37 37 #[route("/")] 38 38 pub struct Index; 39 39 40 - impl Route<PageParams, Markup> for Index { 41 - fn render(&self, ctx: &mut PageContext) -> Markup { 40 + impl Route for Index { 41 + fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> { 42 42 let logo = ctx.add_image("./logo.png"); 43 43 44 44 html! {
+109 -4
website/src/layout.rs
··· 77 77 ) -> impl Into<RenderResult> { 78 78 layout( 79 79 html! { 80 - div.container.mx-auto."grid-cols-(--docs-columns)".grid."min-h-[calc(100%-64px)]" { 81 - aside.bg-linear-to-l."from-darker-white"."py-8"."h-full".border-r.border-r-borders { 80 + // Second header for docs navigation (mobile only) 81 + header.bg-our-white.border-b.border-borders.sm:hidden.bg-linear-to-b."from-darker-white" { 82 + div.flex.items-center.justify-between { 83 + button id="left-sidebar-toggle" .px-4.py-3.flex.items-center.gap-x-2.text-base.font-medium.text-our-black aria-label="Toggle navigation menu" { 84 + (PreEscaped(include_str!("../assets/side-menu.svg"))) 85 + span { "Menu" } 86 + } 87 + button id="right-sidebar-toggle" .px-4.py-3.flex.items-center.gap-x-2.text-base.font-medium.text-our-black aria-label="Toggle table of contents" { 88 + span { "On this page" } 89 + (PreEscaped(include_str!("../assets/toc.svg"))) 90 + } 91 + } 92 + } 93 + 94 + // Mobile left sidebar overlay 95 + div id="mobile-left-sidebar" .fixed.left-0.w-full.bg-our-white.transform."-translate-x-full".transition-all.opacity-0.pointer-events-none.z-50.overflow-y-auto style="top: 116px; bottom: 0;" { 96 + div.px-6.py-4 { 97 + (left_sidebar(ctx)) 98 + } 99 + } 100 + 101 + // Mobile right sidebar overlay 102 + div id="mobile-right-sidebar" .fixed.right-0.w-full.bg-our-white.transform."translate-x-full".transition-all.opacity-0.pointer-events-none.z-50.overflow-y-auto style="top: 116px; bottom: 0;" { 103 + div.px-6.py-4 { 104 + (right_sidebar(headings)) 105 + } 106 + } 107 + 108 + div.container.mx-auto."sm:grid-cols-(--docs-columns)".sm:grid."min-h-[calc(100%-64px)]".px-4.sm:px-0.pt-2.sm:pt-0 { 109 + aside.bg-linear-to-l."from-darker-white"."py-8"."h-full".border-r.border-r-borders.hidden.sm:block { 82 110 (left_sidebar(ctx)) 83 111 } 84 - main.w-full.max-w-larger-prose.mx-auto.py-8 { 112 + main.w-full.max-w-larger-prose.mx-auto.sm:py-8.py-4 { 85 113 (main) 86 114 } 87 - aside."py-8" { 115 + aside."py-8".hidden."sm:block" { 88 116 (right_sidebar(headings)) 89 117 } 118 + } 119 + 120 + script { 121 + (PreEscaped(r#" 122 + document.addEventListener('DOMContentLoaded', function() { 123 + const leftSidebarToggle = document.getElementById('left-sidebar-toggle'); 124 + const rightSidebarToggle = document.getElementById('right-sidebar-toggle'); 125 + const leftSidebar = document.getElementById('mobile-left-sidebar'); 126 + const rightSidebar = document.getElementById('mobile-right-sidebar'); 127 + 128 + let leftOpen = false; 129 + let rightOpen = false; 130 + 131 + function toggleLeftSidebar() { 132 + leftOpen = !leftOpen; 133 + 134 + leftSidebar.classList.toggle('-translate-x-full', !leftOpen); 135 + leftSidebar.classList.toggle('translate-x-0', leftOpen); 136 + leftSidebar.classList.toggle('opacity-0', !leftOpen); 137 + leftSidebar.classList.toggle('opacity-100', leftOpen); 138 + leftSidebar.classList.toggle('pointer-events-none', !leftOpen); 139 + 140 + if (leftOpen) { 141 + document.body.style.overflow = 'hidden'; 142 + } else if (!rightOpen) { 143 + document.body.style.overflow = ''; 144 + } 145 + } 146 + 147 + function toggleRightSidebar() { 148 + rightOpen = !rightOpen; 149 + 150 + rightSidebar.classList.toggle('translate-x-full', !rightOpen); 151 + rightSidebar.classList.toggle('translate-x-0', rightOpen); 152 + rightSidebar.classList.toggle('opacity-0', !rightOpen); 153 + rightSidebar.classList.toggle('opacity-100', rightOpen); 154 + rightSidebar.classList.toggle('pointer-events-none', !rightOpen); 155 + 156 + if (rightOpen) { 157 + document.body.style.overflow = 'hidden'; 158 + } else if (!leftOpen) { 159 + document.body.style.overflow = ''; 160 + } 161 + } 162 + 163 + // Close sidebars when clicking outside 164 + function closeSidebars(event) { 165 + if (leftOpen && !leftSidebar.contains(event.target) && !leftSidebarToggle.contains(event.target)) { 166 + toggleLeftSidebar(); 167 + } 168 + if (rightOpen && !rightSidebar.contains(event.target) && !rightSidebarToggle.contains(event.target)) { 169 + toggleRightSidebar(); 170 + } 171 + } 172 + 173 + leftSidebarToggle.addEventListener('click', toggleLeftSidebar); 174 + rightSidebarToggle.addEventListener('click', toggleRightSidebar); 175 + document.addEventListener('click', closeSidebars); 176 + 177 + // Close right sidebar when clicking on table of contents links 178 + rightSidebar.addEventListener('click', function(event) { 179 + if (event.target.tagName === 'A' && event.target.getAttribute('href').startsWith('#')) { 180 + if (rightOpen) { 181 + toggleRightSidebar(); 182 + } 183 + } 184 + }); 185 + 186 + // Close sidebars on escape key 187 + document.addEventListener('keydown', function(event) { 188 + if (event.key === 'Escape') { 189 + if (leftOpen) toggleLeftSidebar(); 190 + if (rightOpen) toggleRightSidebar(); 191 + } 192 + }); 193 + }); 194 + "#)) 90 195 } 91 196 }, 92 197 true,
+20 -10
website/src/layout/docs_sidebars.rs
··· 37 37 38 38 let entries = sections.iter().map(|(section, entries)| { 39 39 html! { 40 - li.mb-4 { 41 - h2.text-lg.font-bold { (section) } 40 + li.mb-6.sm:mb-4 { 41 + h2.text-xl.sm:text-lg.font-bold { (section) } 42 42 ul { 43 43 @for entry in entries { 44 44 @let url = &format!("/docs/{}", entry.id); 45 45 @let is_current_page = url == ctx.current_path; 46 - li."border-l-2"."hover:border-brand-red"."pl-3"."py-1".(if is_current_page { "text-brand-red border-brand-red" } else { "border-borders" }) { 47 - a.block href=(format!("/docs/{}/", entry.id)) { (entry.data(ctx).title) } // TODO: Use type-safe routing 46 + li { 47 + @let base_classes = "block py-2 sm:py-1 px-4 sm:px-3 text-lg sm:text-base font-medium sm:font-normal transition-colors border-b border-borders sm:border-0"; 48 + @let conditional_classes = if is_current_page { 49 + "text-brand-red sm:border-l-2 sm:border-l-brand-red" 50 + } else { 51 + "text-our-black hover:text-brand-red sm:border-l-2 sm:border-l-borders sm:hover:border-l-brand-red" 52 + }; 53 + a class=(format!("{} {}", base_classes, conditional_classes)) href=(format!("/docs/{}/", entry.id)) { 54 + (entry.data(ctx).title) 55 + } 48 56 } 49 57 } 50 58 } ··· 53 61 }); 54 62 55 63 html! { 56 - ul.mb-4 { 64 + ul.mb-6.sm:mb-4.space-y-0.sm:space-y-1 { 57 65 @for (name, link) in static_links { 58 - li.mb-1 { 59 - a.text-lg href=(link) { (name) } 66 + li { 67 + a.block.py-2.sm:py-0.px-4.sm:px-0.text-xl.sm:text-lg.font-medium.text-our-black.border-b.border-borders.sm:border-0.transition-colors."hover:text-brand-red".sm:bg-transparent.sm:hover:bg-transparent href=(link) { (name) } 60 68 } 61 69 } 62 70 } 63 - ul { 71 + ul.space-y-1 { 64 72 @for entry in entries { 65 73 (entry) 66 74 } ··· 97 105 } 98 106 html_headings.push(html! { 99 107 li.(pad).(border).(margin_top) { 100 - a href=(format!("#{}", heading.id)) { (heading.title) } 108 + a.block.py-1.px-3.sm:px-0.sm:py-0.text-lg.sm:text-base.transition-colors."hover:bg-gray-50".sm:hover:bg-transparent."hover:text-brand-red" href=(format!("#{}", heading.id)) { 109 + (heading.title) 110 + } 101 111 } 102 112 }); 103 113 i += 1; 104 114 } 105 115 106 116 html!( 107 - h2.text-lg.font-bold { "On This Page" } 117 + h2.text-xl.sm:text-lg.font-bold.mb-4.sm:mb-0 { "On This Page" } 108 118 nav.sticky.top-8 { 109 119 ul { 110 120 @for heading in html_headings {
+82 -12
website/src/layout/header.rs
··· 5 5 6 6 pub fn header(_: &mut PageContext, bottom_border: bool) -> Markup { 7 7 let border = if bottom_border { "border-b" } else { "" }; 8 + let nav_links = vec![ 9 + ("/docs/", "Documentation"), 10 + ("/news/", "News"), 11 + ("/contribute/", "Contribute"), 12 + ("https://github.com/bruits/maudit/issues/1", "Roadmap"), 13 + ]; 14 + let social_links = vec![ 15 + ( 16 + "/chat/", 17 + "Join our Discord", 18 + include_str!("../../assets/discord.svg"), 19 + ), 20 + ( 21 + "https://github.com/bruits/maudit", 22 + "View on GitHub", 23 + include_str!("../../assets/github.svg"), 24 + ), 25 + ]; 8 26 9 27 html! { 10 - header.px-8.py-4.text-our-black.bg-our-white."border-borders".(border) { 28 + header.px-4.sm:px-8.py-4.text-our-black.bg-our-white."border-borders".(border) { 11 29 div.container.flex.items-center.mx-auto.justify-between { 12 30 div.flex.items-center.gap-x-8 { 13 31 a.flex.gap-x-2.items-center href="/" { ··· 15 33 h1.text-2xl.tracking-wide { "Maudit" } 16 34 } 17 35 nav.text-lg.gap-x-12.relative."top-[2px]".hidden."sm:flex" { 18 - a href="/docs/" { "Documentation" } 19 - a href="/news/" { "News" } 20 - a href="/contribute/" { "Contribute" } 21 - a href="https://github.com/bruits/maudit/issues/1" { "Roadmap" } 36 + @for (href, text) in &nav_links { 37 + a href=(href) { (text) } 38 + } 22 39 } 23 40 } 24 41 25 - div.flex.gap-x-6 { 26 - a href="/chat/" { 27 - span.sr-only { "Join the Maudit community on Discord" } 28 - (PreEscaped(include_str!("../../assets/discord.svg"))) 42 + div.gap-x-6.hidden.sm:flex { 43 + @for (href, _text, icon_svg) in &social_links { 44 + a href=(href) { 45 + span.sr-only { (_text) } 46 + (PreEscaped(icon_svg)) 47 + } 29 48 } 30 - a href="https://github.com/bruits/maudit" { 31 - span.sr-only { "View Maudit on GitHub" } 32 - (PreEscaped(include_str!("../../assets/github.svg"))) 49 + } 50 + 51 + div.sm:hidden.flex.align-middle.justify-center.items-center { 52 + button id="mobile-menu-button" aria-label="Toggle main menu" { 53 + span id="hamburger-icon" { 54 + (PreEscaped(include_str!("../../assets/hamburger.svg"))) 55 + } 56 + span id="close-icon" .hidden { 57 + (PreEscaped(include_str!("../../assets/close.svg"))) 58 + } 33 59 } 34 60 } 35 61 } 62 + } 63 + 64 + // Mobile menu panel 65 + div id="mobile-menu-panel" .fixed.left-0.w-full.bg-our-white.transform."-translate-x-4".transition-all.opacity-0.pointer-events-none.z-50 style="top: 65px; bottom: 0;" { 66 + nav { 67 + @for (href, text) in &nav_links { 68 + a.block.text-2xl.font-medium.text-our-black.px-4.py-4.border-b.border-borders href=(href) { (text) } 69 + } 70 + } 71 + div.px-6.py-8.flex.flex-wrap.gap-8 { 72 + @for (href, text, icon_svg) in &social_links { 73 + a.flex.items-center href=(href) { 74 + span.sr-only { (text) } 75 + (PreEscaped(icon_svg)) 76 + } 77 + } 78 + } 79 + } 80 + 81 + script { 82 + (PreEscaped(r#" 83 + document.addEventListener('DOMContentLoaded', function() { 84 + const menuButton = document.getElementById('mobile-menu-button'); 85 + const panel = document.getElementById('mobile-menu-panel'); 86 + const hamburgerIcon = document.getElementById('hamburger-icon'); 87 + const closeIcon = document.getElementById('close-icon'); 88 + let isOpen = false; 89 + 90 + function toggleMenu() { 91 + isOpen = !isOpen; 92 + 93 + panel.classList.toggle('-translate-x-4', !isOpen); 94 + panel.classList.toggle('opacity-0', !isOpen); 95 + panel.classList.toggle('pointer-events-none', !isOpen); 96 + 97 + hamburgerIcon.classList.toggle('hidden', isOpen); 98 + closeIcon.classList.toggle('hidden', !isOpen); 99 + 100 + document.body.style.overflow = isOpen ? 'hidden' : ''; 101 + } 102 + 103 + menuButton.addEventListener('click', toggleMenu); 104 + }); 105 + "#)) 36 106 } 37 107 } 38 108 }
+1 -1
website/src/routes/docs.rs
··· 45 45 h3.text-lg { (description) } 46 46 } 47 47 } 48 - section.prose."lg:prose-lg".max-w-none { 48 + section.prose.prose-lg.max-w-none { 49 49 (PreEscaped(entry.render(ctx))) 50 50 } 51 51 }
+2 -2
website/src/routes/news.rs
··· 36 36 37 37 layout( 38 38 html! { 39 - div.container.mx-auto."px-24"."py-10"."pb-24".flex { 39 + div.container.mx-auto."px-4"."sm:px-24"."py-10"."pb-24".flex { 40 40 div.flex-1 { 41 41 @for (year, articles) in articles_by_year.iter().rev() { 42 42 h2.text-3xl.font-bold.mb-4#(year) { (year) } ··· 133 133 } 134 134 } 135 135 136 - section.prose."lg:prose-lg".max-w-none { 136 + section.prose.prose-lg.max-w-none { 137 137 (PreEscaped(entry.render(ctx))) 138 138 } 139 139