Signed by hannaSSH key fingerprint: SHA256:4g9cWhkAAw8gwqhJUcVbSnGEvdGwklW+1Aa/fMUu59k
assets/base.css +33 −12
| @@ -51,6 +51,14 @@ code, pre, .mono { | |||
| 51 | font-family: var(--fb-font-mono); | 51 | font-family: var(--fb-font-mono); |
| 52 | } | 52 | } |
| 53 | 53 | ||
| 54 | /* Inline icons inherit colour and scale to the current font size. */ | ||
| 55 | .icon { | ||
| 56 | width: 1em; | ||
| 57 | height: 1em; | ||
| 58 | flex: none; | ||
| 59 | vertical-align: -0.125em; | ||
| 60 | } | ||
| 61 | |||
| 54 | /* Accessibility: visible focus, skip link. */ | 62 | /* Accessibility: visible focus, skip link. */ |
| 55 | :focus-visible { | 63 | :focus-visible { |
| 56 | outline: 2px solid var(--fb-accent); | 64 | outline: 2px solid var(--fb-accent); |
| @@ -126,6 +134,10 @@ code, pre, .mono { | |||
| 126 | .icon-btn:hover { | 134 | .icon-btn:hover { |
| 127 | background: var(--fb-bg-inset); | 135 | background: var(--fb-bg-inset); |
| 128 | } | 136 | } |
| 137 | .icon-btn .icon { | ||
| 138 | width: 1.15rem; | ||
| 139 | height: 1.15rem; | ||
| 140 | } | ||
| 129 | 141 | ||
| 130 | /* ---- Drawer (off-canvas nav) ---- */ | 142 | /* ---- Drawer (off-canvas nav) ---- */ |
| 131 | .drawer-backdrop { | 143 | .drawer-backdrop { |
| @@ -161,12 +173,27 @@ body.drawer-open .drawer-backdrop { | |||
| 161 | } | 173 | } |
| 162 | .drawer nav a, | 174 | .drawer nav a, |
| 163 | .drawer .drawer-item { | 175 | .drawer .drawer-item { |
| 164 | display: block; | 176 | display: flex; |
| 177 | align-items: center; | ||
| 178 | gap: 0.6rem; | ||
| 179 | width: 100%; | ||
| 165 | padding: 0.4rem 0.5rem; | 180 | padding: 0.4rem 0.5rem; |
| 166 | border-radius: var(--fb-radius); | 181 | border-radius: var(--fb-radius); |
| 167 | color: var(--fb-fg); | 182 | color: var(--fb-fg); |
| 183 | font: inherit; | ||
| 184 | text-align: left; | ||
| 185 | } | ||
| 186 | .drawer .drawer-item { | ||
| 187 | border: none; | ||
| 188 | background: none; | ||
| 189 | cursor: pointer; | ||
| 190 | } | ||
| 191 | .drawer nav a .icon, | ||
| 192 | .drawer .drawer-item .icon { | ||
| 193 | color: var(--fb-fg-muted); | ||
| 168 | } | 194 | } |
| 169 | .drawer nav a:hover { | 195 | .drawer nav a:hover, |
| 196 | .drawer .drawer-item:hover { | ||
| 170 | background: var(--fb-bg-inset); | 197 | background: var(--fb-bg-inset); |
| 171 | text-decoration: none; | 198 | text-decoration: none; |
| 172 | } | 199 | } |
| @@ -398,6 +425,10 @@ table.tree td { | |||
| 398 | .tree-icon { | 425 | .tree-icon { |
| 399 | width: 1.5rem; | 426 | width: 1.5rem; |
| 400 | text-align: center; | 427 | text-align: center; |
| 428 | color: var(--fb-fg-muted); | ||
| 429 | } | ||
| 430 | .tree-icon .icon { | ||
| 431 | vertical-align: -0.15em; | ||
| 401 | } | 432 | } |
| 402 | 433 | ||
| 403 | .blob-header { | 434 | .blob-header { |
| @@ -567,16 +598,6 @@ td.diff-empty { | |||
| 567 | } | 598 | } |
| 568 | } | 599 | } |
| 569 | 600 | ||
| 570 | /* ---- Footer ---- */ | ||
| 571 | .footer { | ||
| 572 | max-width: 76rem; | ||
| 573 | margin: 2rem auto 1rem; | ||
| 574 | padding: 1rem var(--fb-gap) 0; | ||
| 575 | border-top: 1px solid var(--fb-border); | ||
| 576 | color: var(--fb-fg-muted); | ||
| 577 | font-size: 0.85em; | ||
| 578 | } | ||
| 579 | |||
| 580 | /* ---- Responsive: single breakpoint at 48rem ---- */ | 601 | /* ---- Responsive: single breakpoint at 48rem ---- */ |
| 581 | @media (max-width: 48rem) { | 602 | @media (max-width: 48rem) { |
| 582 | .topbar .tabs { | 603 | .topbar .tabs { |
config.toml +73 −0
| @@ -0,0 +1,73 @@ | |||
| 1 | [instance] | ||
| 2 | name = "fabrica" | ||
| 3 | url = "http://localhost:8080" | ||
| 4 | description = "" | ||
| 5 | default_branch = "main" | ||
| 6 | allow_anonymous = true | ||
| 7 | |||
| 8 | [server] | ||
| 9 | address = "0.0.0.0" | ||
| 10 | port = 8080 | ||
| 11 | behind_proxy = false | ||
| 12 | graceful_shutdown_secs = 20 | ||
| 13 | max_body_bytes = 1048576 | ||
| 14 | |||
| 15 | [ssh] | ||
| 16 | enabled = true | ||
| 17 | address = "0.0.0.0" | ||
| 18 | port = 2222 | ||
| 19 | host_key_path = "data/host_key" | ||
| 20 | clone_host = "localhost" | ||
| 21 | clone_port = 22 | ||
| 22 | |||
| 23 | [storage] | ||
| 24 | data_dir = "data/fabrica" | ||
| 25 | repo_dir = "data/repos" | ||
| 26 | |||
| 27 | [database] | ||
| 28 | url = "sqlite://data/fabrica.db" | ||
| 29 | max_connections = 16 | ||
| 30 | |||
| 31 | [auth] | ||
| 32 | session_ttl_days = 30 | ||
| 33 | token_ttl_days = 90 | ||
| 34 | invite_ttl_hours = 72 | ||
| 35 | cookie_name = "fabrica_session" | ||
| 36 | cookie_secure = true | ||
| 37 | argon2_m_cost = 19456 | ||
| 38 | argon2_t_cost = 2 | ||
| 39 | argon2_p_cost = 1 | ||
| 40 | |||
| 41 | [mail] | ||
| 42 | backend = "stdout" | ||
| 43 | from = "fabrica@localhost" | ||
| 44 | host = "" | ||
| 45 | port = 587 | ||
| 46 | username = "" | ||
| 47 | encryption = "starttls" | ||
| 48 | timeout_secs = 15 | ||
| 49 | |||
| 50 | [ui] | ||
| 51 | theme = "dark" | ||
| 52 | themes_dir = "data/themes" | ||
| 53 | assets_dir = "data/assets" | ||
| 54 | allow_theme_choice = true | ||
| 55 | show_runs = false | ||
| 56 | diff_context_lines = 3 | ||
| 57 | max_highlight_bytes = 1048576 | ||
| 58 | max_diff_bytes = 5242880 | ||
| 59 | tree_history_limit = 1000 | ||
| 60 | |||
| 61 | [git] | ||
| 62 | binary = "git" | ||
| 63 | transport_timeout_secs = 3600 | ||
| 64 | max_pack_size_bytes = 536870912 | ||
| 65 | |||
| 66 | [search] | ||
| 67 | concurrency = 4 | ||
| 68 | timeout_secs = 10 | ||
| 69 | max_results = 200 | ||
| 70 | |||
| 71 | [log] | ||
| 72 | level = "info" | ||
| 73 | format = "pretty" | ||
crates/web/src/icons.rs +137 −0
| @@ -0,0 +1,137 @@ | |||
| 1 | // This Source Code Form is subject to the terms of the Mozilla Public | ||
| 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. | ||
| 4 | |||
| 5 | //! Inline SVG icons. | ||
| 6 | //! | ||
| 7 | //! Vendored from [Lucide](https://lucide.dev) (ISC) for UI glyphs and | ||
| 8 | //! [Simple Icons](https://simpleicons.org) (CC0) for brands, inlined so no page | ||
| 9 | //! makes a network request. Icons inherit `currentColor` and size to `1em`. | ||
| 10 | |||
| 11 | use maud::{Markup, PreEscaped, html}; | ||
| 12 | |||
| 13 | /// A vendored icon. | ||
| 14 | // The full glyph set is vendored up front; the profile, explore, and settings | ||
| 15 | // views wire in the remaining variants. Removed once every variant is consumed. | ||
| 16 | #[allow(dead_code)] | ||
| 17 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
| 18 | pub enum Icon { | ||
| 19 | PanelLeft, | ||
| 20 | House, | ||
| 21 | Folder, | ||
| 22 | File, | ||
| 23 | GitBranch, | ||
| 24 | Tag, | ||
| 25 | Search, | ||
| 26 | Copy, | ||
| 27 | ChevronRight, | ||
| 28 | Box, | ||
| 29 | Link, | ||
| 30 | User, | ||
| 31 | Settings, | ||
| 32 | LogOut, | ||
| 33 | Upload, | ||
| 34 | Plus, | ||
| 35 | Dashboard, | ||
| 36 | Compass, | ||
| 37 | MapPin, | ||
| 38 | Globe, | ||
| 39 | Download, | ||
| 40 | AtSign, | ||
| 41 | Github, | ||
| 42 | Mastodon, | ||
| 43 | } | ||
| 44 | |||
| 45 | impl Icon { | ||
| 46 | /// Whether the icon is a filled brand glyph rather than a stroked Lucide one. | ||
| 47 | fn brand(self) -> bool { | ||
| 48 | matches!(self, Icon::Github | Icon::Mastodon) | ||
| 49 | } | ||
| 50 | |||
| 51 | /// The inner SVG geometry. | ||
| 52 | fn geometry(self) -> &'static str { | ||
| 53 | match self { | ||
| 54 | Icon::PanelLeft => { | ||
| 55 | r#"<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/>"# | ||
| 56 | } | ||
| 57 | Icon::House => { | ||
| 58 | r#"<path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8"/><path d="M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>"# | ||
| 59 | } | ||
| 60 | Icon::Folder => { | ||
| 61 | r#"<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/>"# | ||
| 62 | } | ||
| 63 | Icon::File => { | ||
| 64 | r#"<path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/><path d="M14 2v5a1 1 0 0 0 1 1h5"/>"# | ||
| 65 | } | ||
| 66 | Icon::GitBranch => { | ||
| 67 | r#"<path d="M15 6a9 9 0 0 0-9 9V3"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/>"# | ||
| 68 | } | ||
| 69 | Icon::Tag => { | ||
| 70 | r#"<path d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z"/><circle cx="7.5" cy="7.5" r=".5" fill="currentColor"/>"# | ||
| 71 | } | ||
| 72 | Icon::Search => r#"<path d="m21 21-4.34-4.34"/><circle cx="11" cy="11" r="8"/>"#, | ||
| 73 | Icon::Copy => { | ||
| 74 | r#"<rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>"# | ||
| 75 | } | ||
| 76 | Icon::ChevronRight => r#"<path d="m9 18 6-6-6-6"/>"#, | ||
| 77 | Icon::Box => { | ||
| 78 | r#"<path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/>"# | ||
| 79 | } | ||
| 80 | Icon::Link => { | ||
| 81 | r#"<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>"# | ||
| 82 | } | ||
| 83 | Icon::User => { | ||
| 84 | r#"<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>"# | ||
| 85 | } | ||
| 86 | Icon::Settings => { | ||
| 87 | r#"<path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915"/><circle cx="12" cy="12" r="3"/>"# | ||
| 88 | } | ||
| 89 | Icon::LogOut => { | ||
| 90 | r#"<path d="m16 17 5-5-5-5"/><path d="M21 12H9"/><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>"# | ||
| 91 | } | ||
| 92 | Icon::Upload => { | ||
| 93 | r#"<path d="M12 3v12"/><path d="m17 8-5-5-5 5"/><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>"# | ||
| 94 | } | ||
| 95 | Icon::Plus => r#"<path d="M5 12h14"/><path d="M12 5v14"/>"#, | ||
| 96 | Icon::Dashboard => { | ||
| 97 | r#"<rect width="7" height="9" x="3" y="3" rx="1"/><rect width="7" height="5" x="14" y="3" rx="1"/><rect width="7" height="9" x="14" y="12" rx="1"/><rect width="7" height="5" x="3" y="16" rx="1"/>"# | ||
| 98 | } | ||
| 99 | Icon::Compass => { | ||
| 100 | r#"<circle cx="12" cy="12" r="10"/><path d="m16.24 7.76-1.804 5.411a2 2 0 0 1-1.265 1.265L7.76 16.24l1.804-5.411a2 2 0 0 1 1.265-1.265z"/>"# | ||
| 101 | } | ||
| 102 | Icon::MapPin => { | ||
| 103 | r#"<path d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"/><circle cx="12" cy="10" r="3"/>"# | ||
| 104 | } | ||
| 105 | Icon::Globe => { | ||
| 106 | r#"<circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/>"# | ||
| 107 | } | ||
| 108 | Icon::Download => { | ||
| 109 | r#"<path d="M12 15V3"/><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10 5 5 5-5"/>"# | ||
| 110 | } | ||
| 111 | Icon::AtSign => { | ||
| 112 | r#"<circle cx="12" cy="12" r="4"/><path d="M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-4 8"/>"# | ||
| 113 | } | ||
| 114 | Icon::Github => { | ||
| 115 | r#"<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/>"# | ||
| 116 | } | ||
| 117 | Icon::Mastodon => { | ||
| 118 | r#"<path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"/>"# | ||
| 119 | } | ||
| 120 | } | ||
| 121 | } | ||
| 122 | } | ||
| 123 | |||
| 124 | /// Render an icon inheriting `currentColor` and sized to `1em`. | ||
| 125 | #[must_use] | ||
| 126 | pub fn icon(name: Icon) -> Markup { | ||
| 127 | let brand = name.brand(); | ||
| 128 | html! { | ||
| 129 | svg class="icon" viewBox="0 0 24 24" width="1em" height="1em" | ||
| 130 | fill=(if brand { "currentColor" } else { "none" }) | ||
| 131 | stroke=(if brand { "none" } else { "currentColor" }) | ||
| 132 | stroke-width="2" stroke-linecap="round" stroke-linejoin="round" | ||
| 133 | aria-hidden="true" focusable="false" { | ||
| 134 | (PreEscaped(name.geometry())) | ||
| 135 | } | ||
| 136 | } | ||
| 137 | } | ||
crates/web/src/layout.rs +14 −22
| @@ -2,7 +2,7 @@ | |||
| 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this | 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this |
| 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. | 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. |
| 4 | 4 | ||
| 5 | //! The page shell: `<head>`, top bar, off-canvas drawer, theme picker, footer. | 5 | //! The page shell: `<head>`, top bar, off-canvas drawer, theme picker. |
| 6 | //! | 6 | //! |
| 7 | //! Every full page is wrapped by [`page`]; htmx partials return fragments instead. | 7 | //! Every full page is wrapped by [`page`]; htmx partials return fragments instead. |
| 8 | //! Structure is server-rendered and works without JavaScript — the drawer is a | 8 | //! Structure is server-rendered and works without JavaScript — the drawer is a |
| @@ -13,6 +13,7 @@ use maud::{DOCTYPE, Markup, html}; | |||
| 13 | use model::User; | 13 | use model::User; |
| 14 | 14 | ||
| 15 | use crate::assets::{Scheme, ThemeInfo}; | 15 | use crate::assets::{Scheme, ThemeInfo}; |
| 16 | use crate::icons::{Icon, icon}; | ||
| 16 | 17 | ||
| 17 | /// The per-request chrome data the shell needs. | 18 | /// The per-request chrome data the shell needs. |
| 18 | pub struct Chrome { | 19 | pub struct Chrome { |
| @@ -59,7 +60,6 @@ pub fn page(chrome: &Chrome, title: &str, body: Markup) -> Markup { | |||
| 59 | (drawer(chrome)) | 60 | (drawer(chrome)) |
| 60 | div class="drawer-backdrop" {} | 61 | div class="drawer-backdrop" {} |
| 61 | main id="main" class="page" { (body) } | 62 | main id="main" class="page" { (body) } |
| 62 | (footer(chrome)) | ||
| 63 | } | 63 | } |
| 64 | } | 64 | } |
| 65 | } | 65 | } |
| @@ -70,10 +70,11 @@ fn topbar(chrome: &Chrome) -> Markup { | |||
| 70 | html! { | 70 | html! { |
| 71 | header class="topbar" { | 71 | header class="topbar" { |
| 72 | button class="icon-btn" type="button" data-drawer-toggle aria-label="Menu" | 72 | button class="icon-btn" type="button" data-drawer-toggle aria-label="Menu" |
| 73 | aria-controls="drawer" aria-expanded="false" { "☰" } | 73 | aria-controls="drawer" aria-expanded="false" { (icon(Icon::PanelLeft)) } |
| 74 | span class="slug" { a href="/" { (chrome.instance_name) } } | 74 | span class="slug" { a href="/" { (chrome.instance_name) } } |
| 75 | nav class="tabs" aria-label="Primary" {} | 75 | nav class="tabs" aria-label="Primary" {} |
| 76 | a class="icon-btn" href="/" aria-label="Home" { "⌂" } | 76 | a class="icon-btn" href="/search" aria-label="Search" { (icon(Icon::Search)) } |
| 77 | a class="icon-btn" href="/" aria-label="Home" { (icon(Icon::House)) } | ||
| 77 | } | 78 | } |
| 78 | } | 79 | } |
| 79 | } | 80 | } |
| @@ -84,9 +85,12 @@ fn drawer(chrome: &Chrome) -> Markup { | |||
| 84 | aside id="drawer" class="drawer" aria-label="Navigation" { | 85 | aside id="drawer" class="drawer" aria-label="Navigation" { |
| 85 | h2 { (chrome.instance_name) } | 86 | h2 { (chrome.instance_name) } |
| 86 | nav { | 87 | nav { |
| 87 | a href="/" { "Home" } | ||
| 88 | @if let Some(user) = &chrome.user { | 88 | @if let Some(user) = &chrome.user { |
| 89 | a href={ "/" (user.username) } { "Your repositories" } | 89 | a href="/" { (icon(Icon::Dashboard)) span { "Dashboard" } } |
| 90 | a href="/explore" { (icon(Icon::Compass)) span { "Explore" } } | ||
| 91 | a href={ "/" (user.username) } { (icon(Icon::User)) span { "Your profile" } } | ||
| 92 | } @else { | ||
| 93 | a href="/explore" { (icon(Icon::Compass)) span { "Explore" } } | ||
| 90 | } | 94 | } |
| 91 | } | 95 | } |
| 92 | hr; | 96 | hr; |
| @@ -96,16 +100,15 @@ fn drawer(chrome: &Chrome) -> Markup { | |||
| 96 | } | 100 | } |
| 97 | nav { | 101 | nav { |
| 98 | @if chrome.user.is_some() { | 102 | @if chrome.user.is_some() { |
| 99 | a href="/settings" { "Settings" } | 103 | a href="/settings" { (icon(Icon::Settings)) span { "Settings" } } |
| 100 | form method="post" action="/logout" { | 104 | form method="post" action="/logout" { |
| 101 | input type="hidden" name="_csrf" value=(chrome.csrf); | 105 | input type="hidden" name="_csrf" value=(chrome.csrf); |
| 102 | button class="drawer-item" type="submit" | 106 | button class="drawer-item" type="submit" { |
| 103 | style="width:100%;text-align:left;border:none;background:none;color:inherit;cursor:pointer" { | 107 | (icon(Icon::LogOut)) span { "Log out" } |
| 104 | "Log out" | ||
| 105 | } | 108 | } |
| 106 | } | 109 | } |
| 107 | } @else { | 110 | } @else { |
| 108 | a href="/login" { "Sign in" } | 111 | a href="/login" { (icon(Icon::User)) span { "Sign in" } } |
| 109 | } | 112 | } |
| 110 | } | 113 | } |
| 111 | } | 114 | } |
| @@ -131,14 +134,3 @@ fn theme_picker(chrome: &Chrome) -> Markup { | |||
| 131 | } | 134 | } |
| 132 | } | 135 | } |
| 133 | } | 136 | } |
| 134 | |||
| 135 | /// The page footer. | ||
| 136 | fn footer(chrome: &Chrome) -> Markup { | ||
| 137 | html! { | ||
| 138 | footer class="footer" { | ||
| 139 | span { (chrome.instance_name) } | ||
| 140 | " · " | ||
| 141 | a href="/version" { "version" } | ||
| 142 | } | ||
| 143 | } | ||
| 144 | } | ||
crates/web/src/lib.rs +1 −0
| @@ -14,6 +14,7 @@ mod assets; | |||
| 14 | mod diff; | 14 | mod diff; |
| 15 | mod error; | 15 | mod error; |
| 16 | mod git_http; | 16 | mod git_http; |
| 17 | mod icons; | ||
| 17 | mod layout; | 18 | mod layout; |
| 18 | mod markdown; | 19 | mod markdown; |
| 19 | mod pages; | 20 | mod pages; |
crates/web/src/repo.rs +7 −6
| @@ -24,6 +24,7 @@ use serde::Deserialize; | |||
| 24 | use crate::AppState; | 24 | use crate::AppState; |
| 25 | use crate::diff::{self, RenderedFile}; | 25 | use crate::diff::{self, RenderedFile}; |
| 26 | use crate::error::{AppError, AppResult}; | 26 | use crate::error::{AppError, AppResult}; |
| 27 | use crate::icons::{Icon, icon}; | ||
| 27 | use crate::layout::page; | 28 | use crate::layout::page; |
| 28 | use crate::markdown; | 29 | use crate::markdown; |
| 29 | use crate::pages::build_chrome; | 30 | use crate::pages::build_chrome; |
| @@ -1096,7 +1097,7 @@ fn tree_table( | |||
| 1096 | tbody { | 1097 | tbody { |
| 1097 | @for entry in entries { | 1098 | @for entry in entries { |
| 1098 | tr { | 1099 | tr { |
| 1099 | td class="tree-icon" { (entry_icon(entry.kind)) } | 1100 | td class="tree-icon" { (icon(entry_icon(entry.kind))) } |
| 1100 | td { a href=(href(&entry.name, entry.kind)) { (entry.name) } } | 1101 | td { a href=(href(&entry.name, entry.kind)) { (entry.name) } } |
| 1101 | } | 1102 | } |
| 1102 | } | 1103 | } |
| @@ -1106,12 +1107,12 @@ fn tree_table( | |||
| 1106 | } | 1107 | } |
| 1107 | 1108 | ||
| 1108 | /// An icon glyph for a tree entry kind. | 1109 | /// An icon glyph for a tree entry kind. |
| 1109 | fn entry_icon(kind: git::EntryKind) -> &'static str { | 1110 | fn entry_icon(kind: git::EntryKind) -> Icon { |
| 1110 | match kind { | 1111 | match kind { |
| 1111 | git::EntryKind::Directory => "📁", | 1112 | git::EntryKind::Directory => Icon::Folder, |
| 1112 | git::EntryKind::Submodule => "📦", | 1113 | git::EntryKind::Submodule => Icon::Box, |
| 1113 | git::EntryKind::Symlink => "🔗", | 1114 | git::EntryKind::Symlink => Icon::Link, |
| 1114 | git::EntryKind::File => "📄", | 1115 | git::EntryKind::File => Icon::File, |
| 1115 | } | 1116 | } |
| 1116 | } | 1117 | } |
| 1117 | 1118 | ||
data/fabrica/secret +1 −1
| @@ -0,0 +1 @@ | |||
| 0 | \ No newline at end of file | 1 | 3cIBhLUwUan-vB0uok4Ajb2uEJOzapkIB9BwdLB9lXA |
data/host_key +7 −0
| @@ -0,0 +1,7 @@ | |||
| 1 | -----BEGIN OPENSSH PRIVATE KEY----- | ||
| 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW | ||
| 3 | QyNTUxOQAAACDtfdqbDHDx719TofjNfw7zjd2rZtQAace+caQQ1fYOPgAAAIgq2mooKtpq | ||
| 4 | KAAAAAtzc2gtZWQyNTUxOQAAACDtfdqbDHDx719TofjNfw7zjd2rZtQAace+caQQ1fYOPg | ||
| 5 | AAAEDpF5CAVCkyKaFFhvhp2OLdmWtIH33H8XFaS8GhVscL5u192psMcPHvX1Oh+M1/DvON | ||
| 6 | 3atm1ABpx75xpBDV9g4+AAAAAAECAwQF | ||
| 7 | -----END OPENSSH PRIVATE KEY----- | ||