collin/anvil · 5da15b54
Show the TODO board above the file list as a clipped preview
Collin Richards · 2026-08-18 07:28 UTC · 5da15b545f79de52529dcf98d64c9a7f697edde1 · parent 80bf8535 · browse files
modifiedcrates/anvil-web/src/ui.rs+51 −6
| ⋯ 226 unchanged lines | |||
| 227 | 227 | .kanban .card .card-details > :last-child { margin-bottom:0; } | |
| 228 | 228 | .kanban .card .title img { max-width:100%; height:auto; border-radius:4px; } | |
| 229 | 229 | .todo-board-head { font-size:13px; font-weight:600; margin:20px 0 10px; } | |
| 230 | + | /* Repo home: the board leads the page, clipped to a fixed-height teaser that | |
| 231 | + | expands in place. A checkbox drives it, not <details>, because a closed | |
| 232 | + | <details> hides its content outright — there'd be no preview to clip. | |
| 233 | + | `.fits` is set by script when the board is short enough to need neither. */ | |
| 234 | + | .todo-preview { margin:4px 0 16px; } | |
| 235 | + | .todo-preview .todo-board-head { margin-top:0; } | |
| 236 | + | .todo-expand { position:absolute; width:1px; height:1px; opacity:0; } | |
| 237 | + | .todo-preview-body { position:relative; max-height:300px; overflow:hidden; } | |
| 238 | + | .todo-preview-body::after { content:""; position:absolute; left:0; right:0; bottom:0; height:80px; background:linear-gradient(to bottom, transparent, var(--bg)); pointer-events:none; } | |
| 239 | + | .todo-expand:checked ~ .todo-preview-body { max-height:none; } | |
| 240 | + | .todo-expand:checked ~ .todo-preview-body::after { display:none; } | |
| 241 | + | .todo-more { display:inline-block; cursor:pointer; font-size:12px; color:var(--muted); padding:2px 0 6px; user-select:none; } | |
| 242 | + | .todo-more:hover { color:var(--accent); } | |
| 243 | + | .todo-more::after { content:"Show more \25BE"; } | |
| 244 | + | .todo-expand:checked ~ .todo-more::after { content:"Show less \25B4"; } | |
| 245 | + | .todo-expand:focus-visible ~ .todo-more { outline:2px solid var(--accent); outline-offset:2px; border-radius:3px; } | |
| 246 | + | .todo-preview.fits .todo-preview-body { max-height:none; } | |
| 247 | + | .todo-preview.fits .todo-preview-body::after { display:none; } | |
| 248 | + | .todo-preview.fits .todo-more { display:none; } | |
| 230 | 249 | .todo-notes { margin:8px 2px; } | |
| 231 | 250 | .todo-notes > summary { cursor:pointer; font-size:13px; color:var(--muted); } | |
| 232 | 251 | .latest-commit { display:flex; gap:10px; align-items:baseline; background:var(--code-bg); border:1px solid var(--border); border-radius:6px 6px 0 0; border-bottom:0; padding:8px 16px; } | |
| ⋯ 304 unchanged lines | |||
| 537 | 556 | } @else { | |
| 538 | 557 | ul.repo-list { | |
| 539 | 558 | @for r in &repos { | |
| 559 | + | @let path = anvil_core::storage::repo_path(&app.config.repositories_dir(), &r.owner, &r.name); | |
| 560 | + | @let updated = browse::last_commit_time(&path).ok().flatten(); | |
| 540 | 561 | li { | |
| 541 | 562 | div.name { | |
| 542 | 563 | a href=(format!("/{}", r.owner)) { (r.owner) } | |
| ⋯ 3 unchanged lines | |||
| 546 | 567 | @if !r.primary_language.is_empty() { " " span.pill.language { (r.primary_language) } } | |
| 547 | 568 | } | |
| 548 | 569 | @if !r.description.is_empty() { div.muted { (r.description) } } | |
| 570 | + | @if let Some(t) = updated { | |
| 571 | + | div.muted { "Updated " span title=(fmt_time(t)) { (fmt_relative(t)) } } | |
| 572 | + | } | |
| 549 | 573 | } | |
| 550 | 574 | } | |
| 551 | 575 | } | |
| ⋯ 35 unchanged lines | |||
| 587 | 611 | } @else { | |
| 588 | 612 | ul.repo-list { | |
| 589 | 613 | @for r in &visible { | |
| 614 | + | @let path = anvil_core::storage::repo_path(&app.config.repositories_dir(), &owner.username, &r.name); | |
| 615 | + | @let updated = browse::last_commit_time(&path).ok().flatten(); | |
| 590 | 616 | li { | |
| 591 | 617 | div.name { | |
| 592 | 618 | a href=(format!("/{}/{}", owner.username, r.name)) { (r.name) } | |
| ⋯ 1 unchanged line | |||
| 594 | 620 | @if !r.primary_language.is_empty() { " " span.pill.language { (r.primary_language) } } | |
| 595 | 621 | } | |
| 596 | 622 | @if !r.description.is_empty() { div.muted { (r.description) } } | |
| 623 | + | @if let Some(t) = updated { | |
| 624 | + | div.muted { "Updated " span title=(fmt_time(t)) { (fmt_relative(t)) } } | |
| 625 | + | } | |
| 597 | 626 | } | |
| 598 | 627 | } | |
| 599 | 628 | } | |
| ⋯ 594 unchanged lines | |||
| 1194 | 1223 | " · " | |
| 1195 | 1224 | a href=(format!("/{owner}/{repo}/commits/{}", enc_ref(&rev))) { "commits" } | |
| 1196 | 1225 | } | |
| 1226 | + | @if let Some((board, name)) = &todo_board { | |
| 1227 | + | section.todo-preview { | |
| 1228 | + | p.todo-board-head { | |
| 1229 | + | a href=(format!("/{owner}/{repo}/blob/{}/{name}", enc_ref(&rev))) { (name) } | |
| 1230 | + | } | |
| 1231 | + | input.todo-expand #todo-expand type="checkbox"; | |
| 1232 | + | div.todo-preview-body { (board) } | |
| 1233 | + | label.todo-more for="todo-expand" {} | |
| 1234 | + | } | |
| 1235 | + | script { (maud::PreEscaped(TODO_PREVIEW_JS)) } | |
| 1236 | + | } | |
| 1197 | 1237 | @if let Some(c) = &latest { | |
| 1198 | 1238 | div.latest-commit { | |
| 1199 | 1239 | a.sha href=(format!("/{owner}/{repo}/commit/{}", c.id)) { (c.short) } | |
| ⋯ 19 unchanged lines | |||
| 1219 | 1259 | div.md-body { (rendered) } | |
| 1220 | 1260 | } | |
| 1221 | 1261 | } | |
| 1222 | - | @if let Some((board, name)) = &todo_board { | |
| 1223 | - | p.todo-board-head { | |
| 1224 | - | a href=(format!("/{owner}/{repo}/blob/{}/{name}", enc_ref(&rev))) { (name) } | |
| 1225 | - | } | |
| 1226 | - | (board) | |
| 1227 | - | } | |
| 1228 | 1262 | }, | |
| 1229 | 1263 | )) | |
| 1230 | 1264 | } | |
| 1231 | 1265 | ||
| 1266 | + | /// Drops the clip (and its "show more" toggle) when the preview isn't actually | |
| 1267 | + | /// taller than the cap, so a short board doesn't get a pointless teaser. | |
| 1268 | + | const TODO_PREVIEW_JS: &str = r#" | |
| 1269 | + | (function(){ | |
| 1270 | + | var d = document.querySelector('.todo-preview'); | |
| 1271 | + | if (!d) return; | |
| 1272 | + | var body = d.querySelector('.todo-preview-body'); | |
| 1273 | + | if (body.scrollHeight <= body.clientHeight + 8) d.classList.add('fits'); | |
| 1274 | + | })(); | |
| 1275 | + | "#; | |
| 1276 | + | ||
| 1232 | 1277 | async fn tree_root( | |
| 1233 | 1278 | State(app): State<App>, | |
| 1234 | 1279 | user: CurrentUser, | |
| ⋯ 1524 unchanged lines | |||