Signed by hannaSSH key fingerprint: SHA256:4g9cWhkAAw8gwqhJUcVbSnGEvdGwklW+1Aa/fMUu59k
assets/base.css +33 −12
| @@ -51,6 +51,14 @@ code, pre, .mono { | |||
| 51 | 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 | 62 | /* Accessibility: visible focus, skip link. */ | |
| 55 | 63 | :focus-visible { | |
| 56 | 64 | outline: 2px solid var(--fb-accent); | |
| @@ -126,6 +134,10 @@ code, pre, .mono { | |||
| 126 | 134 | .icon-btn:hover { | |
| 127 | 135 | background: var(--fb-bg-inset); | |
| 128 | 136 | } | |
| 137 | + | .icon-btn .icon { | |
| 138 | + | width: 1.15rem; | |
| 139 | + | height: 1.15rem; | |
| 140 | + | } | |
| 129 | 141 | ||
| 130 | 142 | /* ---- Drawer (off-canvas nav) ---- */ | |
| 131 | 143 | .drawer-backdrop { | |
| @@ -161,12 +173,27 @@ body.drawer-open .drawer-backdrop { | |||
| 161 | 173 | } | |
| 162 | 174 | .drawer nav a, | |
| 163 | 175 | .drawer .drawer-item { | |
| 164 | - | display: block; | |
| 176 | + | display: flex; | |
| 177 | + | align-items: center; | |
| 178 | + | gap: 0.6rem; | |
| 179 | + | width: 100%; | |
| 165 | 180 | padding: 0.4rem 0.5rem; | |
| 166 | 181 | border-radius: var(--fb-radius); | |
| 167 | 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 | 197 | background: var(--fb-bg-inset); | |
| 171 | 198 | text-decoration: none; | |
| 172 | 199 | } | |
| @@ -398,6 +425,10 @@ table.tree td { | |||
| 398 | 425 | .tree-icon { | |
| 399 | 426 | width: 1.5rem; | |
| 400 | 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 | 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 | 601 | /* ---- Responsive: single breakpoint at 48rem ---- */ | |
| 581 | 602 | @media (max-width: 48rem) { | |
| 582 | 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 | 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this | |
| 3 | 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 | 7 | //! Every full page is wrapped by [`page`]; htmx partials return fragments instead. | |
| 8 | 8 | //! Structure is server-rendered and works without JavaScript — the drawer is a | |
| @@ -13,6 +13,7 @@ use maud::{DOCTYPE, Markup, html}; | |||
| 13 | 13 | use model::User; | |
| 14 | 14 | ||
| 15 | 15 | use crate::assets::{Scheme, ThemeInfo}; | |
| 16 | + | use crate::icons::{Icon, icon}; | |
| 16 | 17 | ||
| 17 | 18 | /// The per-request chrome data the shell needs. | |
| 18 | 19 | pub struct Chrome { | |
| @@ -59,7 +60,6 @@ pub fn page(chrome: &Chrome, title: &str, body: Markup) -> Markup { | |||
| 59 | 60 | (drawer(chrome)) | |
| 60 | 61 | div class="drawer-backdrop" {} | |
| 61 | 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 | 70 | html! { | |
| 71 | 71 | header class="topbar" { | |
| 72 | 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 | 74 | span class="slug" { a href="/" { (chrome.instance_name) } } | |
| 75 | 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 | 85 | aside id="drawer" class="drawer" aria-label="Navigation" { | |
| 85 | 86 | h2 { (chrome.instance_name) } | |
| 86 | 87 | nav { | |
| 87 | - | a href="/" { "Home" } | |
| 88 | 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 | 96 | hr; | |
| @@ -96,16 +100,15 @@ fn drawer(chrome: &Chrome) -> Markup { | |||
| 96 | 100 | } | |
| 97 | 101 | nav { | |
| 98 | 102 | @if chrome.user.is_some() { | |
| 99 | - | a href="/settings" { "Settings" } | |
| 103 | + | a href="/settings" { (icon(Icon::Settings)) span { "Settings" } } | |
| 100 | 104 | form method="post" action="/logout" { | |
| 101 | 105 | input type="hidden" name="_csrf" value=(chrome.csrf); | |
| 102 | - | button class="drawer-item" type="submit" | |
| 103 | - | style="width:100%;text-align:left;border:none;background:none;color:inherit;cursor:pointer" { | |
| 104 | - | "Log out" | |
| 106 | + | button class="drawer-item" type="submit" { | |
| 107 | + | (icon(Icon::LogOut)) span { "Log out" } | |
| 105 | 108 | } | |
| 106 | 109 | } | |
| 107 | 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 | 14 | mod diff; | |
| 15 | 15 | mod error; | |
| 16 | 16 | mod git_http; | |
| 17 | + | mod icons; | |
| 17 | 18 | mod layout; | |
| 18 | 19 | mod markdown; | |
| 19 | 20 | mod pages; | |
crates/web/src/repo.rs +7 −6
| @@ -24,6 +24,7 @@ use serde::Deserialize; | |||
| 24 | 24 | use crate::AppState; | |
| 25 | 25 | use crate::diff::{self, RenderedFile}; | |
| 26 | 26 | use crate::error::{AppError, AppResult}; | |
| 27 | + | use crate::icons::{Icon, icon}; | |
| 27 | 28 | use crate::layout::page; | |
| 28 | 29 | use crate::markdown; | |
| 29 | 30 | use crate::pages::build_chrome; | |
| @@ -1096,7 +1097,7 @@ fn tree_table( | |||
| 1096 | 1097 | tbody { | |
| 1097 | 1098 | @for entry in entries { | |
| 1098 | 1099 | tr { | |
| 1099 | - | td class="tree-icon" { (entry_icon(entry.kind)) } | |
| 1100 | + | td class="tree-icon" { (icon(entry_icon(entry.kind))) } | |
| 1100 | 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 | 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 | 1111 | match kind { | |
| 1111 | - | git::EntryKind::Directory => "📁", | |
| 1112 | - | git::EntryKind::Submodule => "📦", | |
| 1113 | - | git::EntryKind::Symlink => "🔗", | |
| 1114 | - | git::EntryKind::File => "📄", | |
| 1112 | + | git::EntryKind::Directory => Icon::Folder, | |
| 1113 | + | git::EntryKind::Submodule => Icon::Box, | |
| 1114 | + | git::EntryKind::Symlink => Icon::Link, | |
| 1115 | + | git::EntryKind::File => Icon::File, | |
| 1115 | 1116 | } | |
| 1116 | 1117 | } | |
| 1117 | 1118 | ||
data/fabrica/secret +1 −1
| @@ -0,0 +1 @@ | |||
| 1 | + | 3cIBhLUwUan-vB0uok4Ajb2uEJOzapkIB9BwdLB9lXA | |
| 1 | - | \ No newline at end of file | |
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----- | |