| ⋯ 111 unchanged lines |
| 112 | 112 | | .box .row a.fc-msg { flex:1; margin-left:24px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; text-align:left; color:var(--muted); font-size:13px; } |
| 113 | 113 | | .box .row a.fc-msg:hover { color:var(--accent); } |
| 114 | 114 | | .box .row .fc-time { margin-left:16px; white-space:nowrap; color:var(--muted); font-size:13px; } |
| 115 | | - | .icon { width:16px; flex:none; display:inline-flex; align-items:center; justify-content:center; color:var(--muted); } |
| 115 | + | .icon { width:1em; height:1em; flex:none; fill:currentColor; color:var(--muted); vertical-align:-0.125em; } |
| 116 | 116 | | .icon.dir { color:#54aeff; } |
| 117 | + | .file-actions .btn .icon { color:inherit; } |
| 117 | 118 | | table.code { border-collapse:collapse; width:100%; font:12px/1.45 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; } |
| 118 | 119 | | table.code td { padding:0 10px; vertical-align:top; white-space:pre; } |
| 119 | 120 | | table.code td.ln { text-align:right; color:var(--muted); user-select:none; width:1%; border-right:1px solid var(--border); background:var(--code-bg); } |
| ⋯ 51 unchanged lines |
| 171 | 172 | | form.stack .check { display:flex; gap:8px; align-items:flex-start; max-width:480px; } |
| 172 | 173 | | form.stack select { padding:6px 8px; border:1px solid var(--border); border-radius:6px; font:inherit; } |
| 173 | 174 | | form.stack textarea.editor { max-width:none; font:13px/1.5 ui-monospace,monospace; tab-size:4; resize:vertical; } |
| 174 | | - | p.file-actions { margin:8px 0; } |
| 175 | + | p.file-actions { margin:10px 0; display:flex; gap:6px; align-items:center; } |
| 176 | + | .file-actions .btn { padding:3px 11px; font-size:12px; font-weight:500; border-radius:6px; display:inline-flex; align-items:center; gap:5px; } |
| 175 | 177 | | table.usage { border-collapse:collapse; width:100%; max-width:680px; margin-top:12px; } |
| 176 | 178 | | table.usage th, table.usage td { padding:6px 10px; border-bottom:1px solid var(--border); text-align:left; } |
| 177 | 179 | | table.usage .num { text-align:right; font-variant-numeric:tabular-nums; white-space:nowrap; } |
| ⋯ 62 unchanged lines |
| 240 | 242 | | details.nav-menu { position:relative; } |
| 241 | 243 | | details.nav-menu > summary { list-style:none; cursor:pointer; color:var(--accent); font-size:14px; } |
| 242 | 244 | | details.nav-menu > summary::-webkit-details-marker { display:none; } |
| 243 | | - | details.nav-menu > summary::after { content:" ▾"; font-size:10px; color:var(--muted); } |
| 245 | + | details.nav-menu > summary::after { content:""; display:inline-block; width:0; height:0; margin-left:6px; vertical-align:middle; border:4px solid transparent; border-top:5px solid var(--muted); border-bottom:0; transition:transform .15s ease; } |
| 246 | + | details.nav-menu > summary:hover::after { border-top-color:var(--accent); } |
| 247 | + | details.nav-menu[open] > summary::after { transform:rotate(180deg); } |
| 244 | 248 | | .nav-dropdown { position:absolute; right:0; top:calc(100% + 6px); background:var(--bg); border:1px solid var(--border); border-radius:6px; min-width:130px; box-shadow:0 4px 14px rgba(0,0,0,.1); z-index:200; padding:4px 0; } |
| 245 | 249 | | .nav-dropdown a, .nav-dropdown button { display:block; width:100%; padding:6px 14px; font-size:13px; color:var(--fg); text-align:left; background:none; border:0; cursor:pointer; font:inherit; text-decoration:none; } |
| 246 | 250 | | .nav-dropdown a:hover, .nav-dropdown button:hover { background:var(--code-bg); color:var(--fg); } |
| ⋯ 4 unchanged lines |
| 251 | 255 | | details.rev-menu > summary .pill { cursor:pointer; } |
| 252 | 256 | | "#; |
| 253 | 257 | | |
| 254 | | - | /// Clipboard icon for the clone "copy" button. |
| 255 | | - | const CLIPBOARD_SVG: &str = r#"<svg viewBox="0 0 16 16" width="15" height="15" fill="currentColor" aria-hidden="true"><path d="M10 1.5H6a.5.5 0 0 0-.5.5v1H4A1.5 1.5 0 0 0 2.5 4.5v9A1.5 1.5 0 0 0 4 15h8a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 12 3h-1.5V2a.5.5 0 0 0-.5-.5zm-3.5 1h3v1h-3v-1zM4 4.5h8v9H4v-9z"/></svg>"#; |
| 258 | + | /// Icon set as an SVG sprite (a hidden `<svg>` of `<symbol id="i-…">`s), |
| 259 | + | /// authored in `assets/icons.svg` and embedded at compile time. The layout |
| 260 | + | /// emits it once per page; [`icon`] references a symbol via `<use>`, so the |
| 261 | + | /// path data is never duplicated in the rendered HTML. |
| 262 | + | const ICON_SPRITE: &str = include_str!("../assets/icons.svg"); |
| 263 | + | |
| 264 | + | /// The icons defined in the sprite. Each maps to a `<symbol id="i-…">` in |
| 265 | + | /// `assets/icons.svg` — keep the two in sync. |
| 266 | + | #[derive(Clone, Copy)] |
| 267 | + | pub(crate) enum Icon { |
| 268 | + | Clipboard, |
| 269 | + | Pencil, |
| 270 | + | Plus, |
| 271 | + | Folder, |
| 272 | + | File, |
| 273 | + | } |
| 274 | + | |
| 275 | + | impl Icon { |
| 276 | + | /// The sprite symbol id (`<symbol id="…">`). |
| 277 | + | fn id(self) -> &'static str { |
| 278 | + | match self { |
| 279 | + | Icon::Clipboard => "i-clipboard", |
| 280 | + | Icon::Pencil => "i-pencil", |
| 281 | + | Icon::Plus => "i-plus", |
| 282 | + | Icon::Folder => "i-folder", |
| 283 | + | Icon::File => "i-file", |
| 284 | + | } |
| 285 | + | } |
| 286 | + | } |
| 256 | 287 | | |
| 257 | | - | /// Filled folder icon for directory entries in the tree view. |
| 258 | | - | const FOLDER_SVG: &str = r#"<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M1.75 1A1.75 1.75 0 0 0 0 2.75v10.5C0 14.216.784 15 1.75 15h12.5A1.75 1.75 0 0 0 16 13.25v-8.5A1.75 1.75 0 0 0 14.25 3H7.5a.25.25 0 0 1-.2-.1l-.9-1.2C6.07 1.26 5.55 1 5 1H1.75Z"/></svg>"#; |
| 288 | + | /// Reference a sprite symbol as an inline `<svg>`, sized/colored by the `.icon` |
| 289 | + | /// CSS (1em, `currentColor`). |
| 290 | + | fn icon(i: Icon) -> Markup { |
| 291 | + | icon_with(i, "icon") |
| 292 | + | } |
| 259 | 293 | | |
| 260 | | - | /// Outline file icon for blob entries in the tree view. |
| 261 | | - | const FILE_SVG: &str = r#"<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h9.5a.25.25 0 0 0 .25-.25V6h-2.75A1.75 1.75 0 0 1 9 4.25V1.5Zm6.75.062V4.25c0 .138.112.25.25.25h2.688l-.011-.013-2.914-2.914-.013-.011Z"/></svg>"#; |
| 294 | + | /// Like [`icon`] but with custom classes (e.g. `"icon dir"` to tint a folder). |
| 295 | + | fn icon_with(i: Icon, class: &str) -> Markup { |
| 296 | + | PreEscaped(format!( |
| 297 | + | r##"<svg class="{class}" aria-hidden="true"><use href="#{}"></use></svg>"##, |
| 298 | + | i.id() |
| 299 | + | )) |
| 300 | + | } |
| 262 | 301 | | |
| 263 | 302 | | /// Delegated handlers for the clone widget: protocol toggle + copy-to-clipboard. |
| 264 | 303 | | /// Registered once on `document`, so it survives htmx body swaps. |
| ⋯ 90 unchanged lines |
| 355 | 394 | | style { (PreEscaped(STYLE)) } |
| 356 | 395 | | } |
| 357 | 396 | | body hx-boost="true" hx-headers=[hx_headers] { |
| 397 | + | (PreEscaped(ICON_SPRITE)) |
| 358 | 398 | | header.top { div.container { |
| 359 | 399 | | a.brand href="/" { "anvil" } |
| 360 | 400 | | span style="margin-left:auto" { |
| ⋯ 660 unchanged lines |
| 1021 | 1061 | | div.clone-cmd { |
| 1022 | 1062 | | code { (default_cmd) } |
| 1023 | 1063 | | button.copy-btn type="button" title="Copy to clipboard" aria-label="Copy" { |
| 1024 | | - | (PreEscaped(CLIPBOARD_SVG)) |
| 1064 | + | (icon(Icon::Clipboard)) |
| 1025 | 1065 | | } |
| 1026 | 1066 | | span.copied-msg { "Copied!" } |
| 1027 | 1067 | | } |
| ⋯ 225 unchanged lines |
| 1253 | 1293 | | (breadcrumbs(&owner, &repo, &rev, &path, true)) |
| 1254 | 1294 | | @if can_edit { |
| 1255 | 1295 | | p.file-actions { |
| 1256 | | - | a.btn.btn-secondary href=(format!("/{owner}/{repo}/edit/{}/{path}", enc_ref(&rev))) { "Edit" } |
| 1296 | + | a.btn.btn-secondary href=(format!("/{owner}/{repo}/edit/{}/{path}", enc_ref(&rev))) { |
| 1297 | + | (icon(Icon::Pencil)) "Edit" |
| 1298 | + | } |
| 1257 | 1299 | | @if is_todo { |
| 1258 | | - | " " |
| 1259 | | - | a.btn.btn-secondary href=(format!("/{owner}/{repo}/add-task/{}/{path}", enc_ref(&rev))) { "Add task" } |
| 1300 | + | a.btn.btn-secondary href=(format!("/{owner}/{repo}/add-task/{}/{path}", enc_ref(&rev))) { |
| 1301 | + | (icon(Icon::Plus)) "Add task" |
| 1302 | + | } |
| 1260 | 1303 | | } |
| 1261 | 1304 | | } |
| 1262 | 1305 | | } |
| ⋯ 544 unchanged lines |
| 1807 | 1850 | | |
| 1808 | 1851 | | /// Folder or file icon for an entry row (tree listings, pages, artifacts). |
| 1809 | 1852 | | pub(crate) fn entry_icon(is_dir: bool) -> Markup { |
| 1810 | | - | html! { |
| 1811 | | - | @if is_dir { |
| 1812 | | - | span.icon.dir { (PreEscaped(FOLDER_SVG)) } |
| 1813 | | - | } @else { |
| 1814 | | - | span.icon { (PreEscaped(FILE_SVG)) } |
| 1815 | | - | } |
| 1853 | + | if is_dir { |
| 1854 | + | icon_with(Icon::Folder, "icon dir") |
| 1855 | + | } else { |
| 1856 | + | icon(Icon::File) |
| 1816 | 1857 | | } |
| 1817 | 1858 | | } |
| 1818 | 1859 | | |
| ⋯ 661 unchanged lines |