anvilsign in

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
227227 .kanban .card .card-details > :last-child { margin-bottom:0; }
228228 .kanban .card .title img { max-width:100%; height:auto; border-radius:4px; }
229229 .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; }
230249 .todo-notes { margin:8px 2px; }
231250 .todo-notes > summary { cursor:pointer; font-size:13px; color:var(--muted); }
232251 .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
537556 } @else {
538557 ul.repo-list {
539558 @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();
540561 li {
541562 div.name {
542563 a href=(format!("/{}", r.owner)) { (r.owner) }
⋯ 3 unchanged lines
546567 @if !r.primary_language.is_empty() { " " span.pill.language { (r.primary_language) } }
547568 }
548569 @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+ }
549573 }
550574 }
551575 }
⋯ 35 unchanged lines
587611 } @else {
588612 ul.repo-list {
589613 @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();
590616 li {
591617 div.name {
592618 a href=(format!("/{}/{}", owner.username, r.name)) { (r.name) }
⋯ 1 unchanged line
594620 @if !r.primary_language.is_empty() { " " span.pill.language { (r.primary_language) } }
595621 }
596622 @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+ }
597626 }
598627 }
599628 }
⋯ 594 unchanged lines
11941223 " · "
11951224 a href=(format!("/{owner}/{repo}/commits/{}", enc_ref(&rev))) { "commits" }
11961225 }
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+ }
11971237 @if let Some(c) = &latest {
11981238 div.latest-commit {
11991239 a.sha href=(format!("/{owner}/{repo}/commit/{}", c.id)) { (c.short) }
⋯ 19 unchanged lines
12191259 div.md-body { (rendered) }
12201260 }
12211261 }
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- }
12281262 },
12291263 ))
12301264 }
12311265
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+
12321277 async fn tree_root(
12331278 State(app): State<App>,
12341279 user: CurrentUser,
⋯ 1524 unchanged lines