feat(web): sidebar cards, a shaded tree head, icons, aligned Diffusion tables (closes #152) c9aacc0e7dc1
The repository sidebar is a column of rounded cards with shaded headings. Diffusion is one card, Takes from and Diffuses to, each repository on its own line with its pill beneath; bookmarks are the ten most recently moved with a link to the rest; the hub's delegate key carries the OxygenHub mark and no longer wraps. The tree has a shaded head with the latest change, its author and the history, and folder, file, link and subrepo icons; the site bar has icons. The Diffusion tables share one column grid, and the off half of the pill lost its stray dashed border. Help › Coming from GitHub covers submodules and Renovate/Dependabot.
- Author
- Andy McCall @andymccall <[email protected]>
- When
- · 2026-10-13 13:24 UTC
- Signature
- ✔ signed by Andy McCall
- Hash
c9aacc0e7dc16401299181ea95653dd34f1a0447778c213d09a69d1c9337e61b- Depends on
- 07b1a5bbf481 145306044896 1553bfc9ae53 18e381ea88c2 19bb31ff1f5c 206c1fc1b06a 25ab8c73514a 3f4b0ea6eabf 42fa95540468 43e8ca33bc5d 4edcf24025cf 58f915792b85 64caed18236a 6ad368c5958e 73ef9f767132 749d9a5fcd9f 756fe44a0f4d 757195ad7a5d 83d5052bf76c 8933faff2043 9198231cee96 9e50caedf4db a024e800af4e a7e6e8e6094c b5668108487f d23464c0ac60 e9bfea8e26db f677d6270298
- Depended on by
- 166f6e9bf4f1 e97500b70f9a 16ee5d19d220 a10446c68043 67be8ac00c24 3dd6582de060 ffb67a81188c 35fea4237cc9 6ae3fa0af4f6 d7d182467382 61ecbd299f22 7f283f8e8b97 f8cf54e99879 1656dc14c2ac 4fa0da126b70 08e221b77658 eb2a3502601d
- In bookmarks
- api-unknown-fields cancel-landed-runs diffusion-pin-bookmark draft-ready main oh-comment-body-file proposal-list-fast
- In tags
- v0.5.0 v0.5.1 v0.5.2 v0.6.0 v0.6.1 v0.7.0 v0.8.0 v0.9.0 v0.9.1 v0.9.11 v0.9.15 v0.9.16 v0.9.19 v0.9.2 v0.9.20 v0.9.21 v0.9.22 v0.9.23 v0.9.24 v0.9.3 v0.9.4 v0.9.5 v0.9.6 v0.9.7 v0.9.8 v0.9.9
- Edits
- 56 atoms, 1 tree operation, 15 files
Browse the tree at this change
added crates/hub/src/web/icons.rs +58 −0
| 1 | + | //! Small inline icons (oxygenhub#152): 16-unit SVGs drawn in |
|
| 2 | + | //! `currentColor`, so they take the colour of the text around them. |
|
| 3 | + | ||
| 4 | + | /// The opening tag every icon shares. |
|
| 5 | + | macro_rules! open { |
|
| 6 | + | () => { |
|
| 7 | + | r#"<svg class="ic" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">"# |
|
| 8 | + | }; |
|
| 9 | + | } |
|
| 10 | + | ||
| 11 | + | /// The icon by name, as markup; empty for a name it does not know. |
|
| 12 | + | pub fn svg(name: &str) -> &'static str { |
|
| 13 | + | match name { |
|
| 14 | + | "home" => concat!(open!(), r#"<path d="M2.5 7.5 8 3l5.5 4.5V13a.5.5 0 0 1-.5.5H9.5V10h-3v3.5H3a.5.5 0 0 1-.5-.5z"/></svg>"#), |
|
| 15 | + | "explore" => concat!(open!(), r#"<circle cx="8" cy="8" r="6"/><path d="m10.6 5.4-1.6 3.6-3.6 1.6 1.6-3.6z"/></svg>"#), |
|
| 16 | + | "help" => concat!( |
|
| 17 | + | open!(), |
|
| 18 | + | r#"<circle cx="8" cy="8" r="6"/><path d="M6.3 6.4a1.8 1.8 0 1 1 2.5 1.6c-.5.2-.8.6-.8 1.1v.4"/><circle cx="8" cy="11.4" r=".5" fill="currentColor" stroke="none"/></svg>"# |
|
| 19 | + | ), |
|
| 20 | + | "projects" => concat!(open!(), r#"<rect x="2" y="2.5" width="12" height="11" rx="1.5"/><path d="M6 2.5v11M10 2.5v7"/></svg>"#), |
|
| 21 | + | "bell" => concat!(open!(), r#"<path d="M4 11V7a4 4 0 0 1 8 0v4l1.2 1.5H2.8z"/><path d="M6.6 14a1.5 1.5 0 0 0 2.8 0"/></svg>"#), |
|
| 22 | + | "settings" => concat!( |
|
| 23 | + | open!(), |
|
| 24 | + | r#"<path d="M2 4.5h7M12.5 4.5H14M2 11.5h1.5M7 11.5h7"/><circle cx="10.75" cy="4.5" r="1.75"/><circle cx="5.25" cy="11.5" r="1.75"/></svg>"# |
|
| 25 | + | ), |
|
| 26 | + | "admin" => concat!(open!(), r#"<path d="M8 1.8 13 3.8v3.7c0 3-2.1 5.6-5 6.7-2.9-1.1-5-3.7-5-6.7V3.8z"/></svg>"#), |
|
| 27 | + | "dir" => concat!( |
|
| 28 | + | open!(), |
|
| 29 | + | r#"<path d="M1.75 3h4.1l1.6 1.6h6.8c.4 0 .75.35.75.75v7.15c0 .4-.35.75-.75.75H1.75c-.4 0-.75-.35-.75-.75V3.75c0-.4.35-.75.75-.75z" fill="currentColor" stroke="none"/></svg>"# |
|
| 30 | + | ), |
|
| 31 | + | "file" => concat!( |
|
| 32 | + | open!(), |
|
| 33 | + | r#"<path d="M4 1.75h5l3.5 3.5v8.5a.5.5 0 0 1-.5.5H4a.5.5 0 0 1-.5-.5V2.25a.5.5 0 0 1 .5-.5z"/><path d="M9 1.75v3.5h3.5"/></svg>"# |
|
| 34 | + | ), |
|
| 35 | + | "link" => concat!( |
|
| 36 | + | open!(), |
|
| 37 | + | r#"<path d="M4 1.75h5l3.5 3.5v8.5a.5.5 0 0 1-.5.5H4a.5.5 0 0 1-.5-.5V2.25a.5.5 0 0 1 .5-.5z"/><path d="M6 11.5 9.5 8M7 8h2.5v2.5"/></svg>"# |
|
| 38 | + | ), |
|
| 39 | + | "subrepo" => concat!(open!(), r#"<path d="M2.5 5 8 2.5 13.5 5v6L8 13.5 2.5 11z"/><path d="M2.5 5 8 7.5 13.5 5M8 7.5v6"/></svg>"#), |
|
| 40 | + | "up" => concat!(open!(), r#"<path d="M6 3.5 3 6.5l3 3"/><path d="M3 6.5h6.5a3 3 0 0 1 3 3v3"/></svg>"#), |
|
| 41 | + | "history" => concat!(open!(), r#"<circle cx="8" cy="8" r="6"/><path d="M8 4.5V8l2.5 1.5"/></svg>"#), |
|
| 42 | + | _ => "", |
|
| 43 | + | } |
|
| 44 | + | } |
|
| 45 | + | ||
| 46 | + | #[cfg(test)] |
|
| 47 | + | mod tests { |
|
| 48 | + | #[test] |
|
| 49 | + | fn every_named_icon_is_one_closed_svg() { |
|
| 50 | + | for name in ["home", "explore", "help", "projects", "bell", "settings", "admin", "dir", "file", "link", "subrepo", "up", "history"] |
|
| 51 | + | { |
|
| 52 | + | let s = super::svg(name); |
|
| 53 | + | assert!(s.starts_with("<svg class=\"ic\"") && s.ends_with("</svg>"), "{name}"); |
|
| 54 | + | assert_eq!(s.matches("<svg").count(), 1, "{name}"); |
|
| 55 | + | } |
|
| 56 | + | assert_eq!(super::svg("nothing"), ""); |
|
| 57 | + | } |
|
| 58 | + | } |
modified CHANGELOG.md +2 −1
| … | 3 unchanged lines |
||
| 4 | 4 | ||
| 5 | 5 | ## Unreleased |
|
| 6 | 6 | ||
| 7 | - | - **Diffusion (#148, docs/diffusion.md).** When a repository that others pin as a subrepo moves on, each consumer that follows it gets a proposal moving the pin, signed by the hub, tested by the consumer's own pipeline, with the upstream's changes listed. One proposal per consumer and subrepo, kept up to date; never landed by itself. Turned on per repository (Settings › Diffusion, `PUT /repos/{owner}/{name}/diffusion`, or `follow` in `.oxymodules`), off by default; `green` waits for the upstream's run to pass. A repository's page shows who uses it and how far behind each is (Used by, `GET /repos/{owner}/{name}/consumers`). Consumers are bumped on the bookmark their work lands on, learned from where their proposals land. |
|
| 7 | + | - **Diffusion (#148, docs/diffusion.md).** When a repository that others pin as a subrepo moves on, each consumer that follows it gets a proposal moving the pin, signed by the hub, tested by the consumer's own pipeline, with the upstream's changes listed. One proposal per consumer and subrepo, kept up to date; never landed by itself. Turned on per repository (Settings › Diffusion, `PUT /repos/{owner}/{name}/diffusion`, or `follow` in `.oxymodules`), off by default; `green` waits for the upstream's run to pass. A repository's page shows who uses it and how far behind each is (Diffusion › Diffuses to, `GET /repos/{owner}/{name}/consumers`). Consumers are bumped on the bookmark their work lands on, learned from where their proposals land. |
|
| 8 | + | - **Tidier pages (#152).** The repository sidebar is a column of cards with shaded headings: Diffusion in one card (Takes from and Diffuses to, each with its pill), the ten most recently moved bookmarks with a link to the rest, and the hub's delegate key shown with the OxygenHub mark. The code tree has a shaded head with the latest change and the history, and folder and file icons; the site bar has icons; the Diffusion tables share one column grid; Help › Coming from GitHub covers submodules and Renovate. |
|
| 8 | 9 | - **Admin › Runners › Images (#146):** every runner image, where it is and what uses it. |
|
| 9 | 10 | - **Faster pages after a restart (#145):** bookmark summaries are kept on disk; slow requests are logged. |
|
| 10 | 11 | ||
| … | 162 unchanged lines |
modified assets/github.css +3 −0
| … | 192 unchanged lines |
||
| 193 | 193 | :root[data-look="github"] select:not([multiple]) { padding-right: 30px; } |
|
| 194 | 194 | :root[data-look="github"] button.menu-item.menu-pick { background: none; border: 0; border-bottom: 1px solid var(--line); border-radius: 0; box-shadow: none; padding: 8px 12px; text-transform: none; font-size: 13px; font-weight: 400; text-align: left; } |
|
| 195 | 195 | :root[data-look="github"] button.menu-item.menu-pick:hover { background: var(--soft); } |
|
| 196 | + | /* The tree under its shaded head (#152): the head takes the top corners. */ |
|
| 197 | + | :root[data-look="github"] .tree-box .tree-head + table.tree { border-top-left-radius: 0; border-top-right-radius: 0; } |
|
| 198 | + | :root[data-look="github"] .side-card > h3 { font-size: 14px; } |
modified crates/hub/src/repos/view.rs +21 −1
| … | 906 unchanged lines |
||
| 907 | 907 | pub languages: Vec<LanguageShare>, |
|
| 908 | 908 | } |
|
| 909 | 909 | ||
| 910 | + | impl Overview { |
|
| 911 | + | /// The sidebar's bookmarks (#152): the default first, then the most |
|
| 912 | + | /// recently moved, ten in all. |
|
| 913 | + | pub fn recent_bookmarks(&self) -> Vec<&BookmarkView> { |
|
| 914 | + | let mut list: Vec<&BookmarkView> = self.bookmarks.iter().collect(); |
|
| 915 | + | list.sort_by(|a, b| b.default.cmp(&a.default).then(b.latest_ms.cmp(&a.latest_ms)).then_with(|| a.name.cmp(&b.name))); |
|
| 916 | + | list.truncate(10); |
|
| 917 | + | list |
|
| 918 | + | } |
|
| 919 | + | } |
|
| 920 | + | ||
| 910 | 921 | /// Someone who authored changes in a state (hub spec §6.4). |
|
| 911 | 922 | #[derive(Clone, Debug, PartialEq, Eq)] |
|
| 912 | 923 | pub struct Contributor { |
|
| … | 6 unchanged lines |
||
| 919 | 930 | /// Of `changes`, how many a delegate signed for this author, by the |
|
| 920 | 931 | /// delegate's name (an agent's work stays the author's, and is shown). |
|
| 921 | 932 | pub via: Vec<(String, usize)>, |
|
| 933 | + | /// The hub's own delegate key, filled by the web layer (#152). |
|
| 934 | + | pub hub: bool, |
|
| 922 | 935 | } |
|
| 923 | 936 | ||
| 924 | 937 | /// One language's share of a state's code, by bytes. |
|
| … | 17 unchanged lines |
||
| 942 | 955 | // Unsigned changes with no Git author carry no name at all. |
|
| 943 | 956 | let name = |
|
| 944 | 957 | if a.key.is_empty() && a.email.is_empty() && a.name == "unknown" { "Unattributed changes".to_string() } else { a.name.clone() }; |
|
| 945 | - | let entry = by.entry(id).or_insert_with(|| Contributor { name, key: a.key.clone(), changes: 0, account: None, via: Vec::new() }); |
|
| 958 | + | let entry = by.entry(id).or_insert_with(|| Contributor { |
|
| 959 | + | name, |
|
| 960 | + | key: a.key.clone(), |
|
| 961 | + | changes: 0, |
|
| 962 | + | account: None, |
|
| 963 | + | via: Vec::new(), |
|
| 964 | + | hub: false, |
|
| 965 | + | }); |
|
| 946 | 966 | entry.changes += 1; |
|
| 947 | 967 | if !meta.author.is_zero() { |
|
| 948 | 968 | let signer = repo.store().signatures(hash).ok().and_then(|s| s.first().map(|s| s.key)).filter(|k| *k != meta.author); |
|
| … | 550 unchanged lines |
modified crates/hub/src/web/mod.rs +1 −0
| … | 16 unchanged lines |
||
| 17 | 17 | pub mod help; |
|
| 18 | 18 | pub mod history; |
|
| 19 | 19 | pub mod home; |
|
| 20 | + | pub mod icons; |
|
| 20 | 21 | mod inbox; |
|
| 21 | 22 | pub mod issues; |
|
| 22 | 23 | mod orgs; |
|
| … | 722 unchanged lines |
modified crates/hub/src/web/repo.rs +20 −5
| … | 109 unchanged lines |
||
| 110 | 110 | app.repos.read(&repo, move |r, cache| view::overview(r, cache, &default)).await.map_err(|e| Error::from_repo(shell.clone(), e))?; |
|
| 111 | 111 | // Contributors link to the hub accounts their keys are linked to. |
|
| 112 | 112 | let linked: HashMap<String, String> = crate::db::accounts::identity_accounts(&app.db).await.unwrap_or_default().into_iter().collect(); |
|
| 113 | + | let hub_key = app.hub_key.key.to_hex(); |
|
| 113 | 114 | for c in &mut o.contributors { |
|
| 114 | 115 | c.account = linked.get(&c.key).cloned(); |
|
| 116 | + | c.hub = c.key == hub_key; |
|
| 115 | 117 | } |
|
| 118 | + | if let Some(c) = o.latest.as_mut() { |
|
| 119 | + | c.author.account = linked.get(&c.author.key).cloned(); |
|
| 120 | + | } |
|
| 116 | 121 | // One row per account: a person's keys over time are one contributor, |
|
| 117 | 122 | // their agents' shares summed beneath (as `oxy log` shows via). |
|
| 118 | 123 | let mut merged: Vec<view::Contributor> = Vec::new(); |
|
| … | 319 unchanged lines |
||
| 438 | 443 | } |
|
| 439 | 444 | ||
| 440 | 445 | impl EntryKind { |
|
| 441 | - | pub fn glyph(&self) -> &'static str { |
|
| 446 | + | /// The tree's icon for the entry (oxygenhub#152). |
|
| 447 | + | pub fn icon(&self) -> &'static str { |
|
| 448 | + | super::icons::svg(match self { |
|
| 449 | + | EntryKind::Dir => "dir", |
|
| 450 | + | EntryKind::File => "file", |
|
| 451 | + | EntryKind::Symlink => "link", |
|
| 452 | + | EntryKind::Subrepo => "subrepo", |
|
| 453 | + | }) |
|
| 454 | + | } |
|
| 455 | + | ||
| 456 | + | pub fn class(&self) -> &'static str { |
|
| 442 | 457 | match self { |
|
| 443 | - | EntryKind::Dir => "▸", |
|
| 444 | - | EntryKind::File => "·", |
|
| 445 | - | EntryKind::Symlink => "↪", |
|
| 446 | - | EntryKind::Subrepo => "◫", |
|
| 458 | + | EntryKind::Dir => "ic-dir", |
|
| 459 | + | EntryKind::File => "ic-file", |
|
| 460 | + | EntryKind::Symlink => "ic-link", |
|
| 461 | + | EntryKind::Subrepo => "ic-sub", |
|
| 447 | 462 | } |
|
| 448 | 463 | } |
|
| 449 | 464 | } |
|
| … | 98 unchanged lines |
modified crates/hub/tests/flow.rs +4 −4
| … | 3167 unchanged lines |
||
| 3168 | 3168 | assert!(api.push("origin", false).unwrap().iter().all(|r| r.ok)); |
|
| 3169 | 3169 | assert_eq!(settle(&|l| l.first().is_some_and(|c| c["behind"] == 1)).await[0]["behind"], 1); |
|
| 3170 | 3170 | let (_, page) = call(&h.app, "GET", "/andy/api", None, None).await; |
|
| 3171 | - | assert!(page.contains("Used by") && page.contains("andy/game") && page.contains("1 behind"), "the page shows it"); |
|
| 3171 | + | assert!(page.contains("Diffuses to") && page.contains("andy/game") && page.contains("1 behind"), "the page shows it"); |
|
| 3172 | 3172 | ||
| 3173 | 3173 | // A private consumer is not shown to someone who cannot read it. |
|
| 3174 | 3174 | sqlx::query("UPDATE repositories SET visibility = 'private' WHERE name = 'game'").execute(&h.app.db).await.unwrap(); |
|
| … | 210 unchanged lines |
||
| 3385 | 3385 | pass_all().await; |
|
| 3386 | 3386 | let (st, page) = call(&h.app, "GET", "/andy/api/diffusion", None, None).await; |
|
| 3387 | 3387 | assert_eq!(st, StatusCode::OK); |
|
| 3388 | - | assert!(page.contains("Used by") && page.contains("andy/game") && page.contains(&format!("#{}", p2.number)), "the bump is listed"); |
|
| 3388 | + | assert!(page.contains("Diffuses to") && page.contains("andy/game") && page.contains(&format!("#{}", p2.number)), "the bump is listed"); |
|
| 3389 | 3389 | assert!(page.contains("df-green"), "a passing run of a checked upstream is green"); |
|
| 3390 | 3390 | let (_, page) = call(&h.app, "GET", "/andy/game/diffusion", None, None).await; |
|
| 3391 | - | assert!(page.contains("Follows") && page.contains("andy/api")); |
|
| 3391 | + | assert!(page.contains("Takes from") && page.contains("andy/api")); |
|
| 3392 | 3392 | let (_, overview) = call(&h.app, "GET", "/andy/game", None, None).await; |
|
| 3393 | - | assert!(overview.contains("Follows") && overview.contains(">Diffusion</a>"), "the tab and the panel"); |
|
| 3393 | + | assert!(overview.contains("Takes from") && overview.contains(">Diffusion</a>"), "the tab and the panel"); |
|
| 3394 | 3394 | let (st, _) = call(&h.app, "GET", "/admin/diffusion", None, None).await; |
|
| 3395 | 3395 | assert_eq!(st, StatusCode::NOT_FOUND, "administrators only"); |
|
| 3396 | 3396 | ||
| … | 148 unchanged lines |
modified crates/hub/tests/issues.rs +1 −1
| … | 755 unchanged lines |
||
| 756 | 756 | // a New project button on the list; a stranger is told who can make one. |
|
| 757 | 757 | let home = get(app, "/andy/demo", Some(&andy)).await.body; |
|
| 758 | 758 | assert!( |
|
| 759 | - | home.contains("href=\"/projects/andy\">Projects</a>") && home.contains("title=\"andy's project boards\">Projects</a>"), |
|
| 759 | + | home.contains("href=\"/projects/andy\"><svg class=\"ic\"") && home.contains("title=\"andy's project boards\">Projects</a>"), |
|
| 760 | 760 | "site bar and tab" |
|
| 761 | 761 | ); |
|
| 762 | 762 | let list = get(app, "/projects/andy", Some(&andy)).await.body; |
|
| … | 147 unchanged lines |
modified templates/base.html +6 −6
| … | 28 unchanged lines |
||
| 29 | 29 | </nav> |
|
| 30 | 30 | </header> |
|
| 31 | 31 | <nav class="band" aria-label="Site"> |
|
| 32 | - | <a href="/">{% if shell.user.is_some() %}Dashboard{% else %}Repositories{% endif %}</a> |
|
| 33 | - | <a href="/explore">Explore</a> |
|
| 34 | - | <a href="/help">Help</a> |
|
| 35 | - | {% if let Some(u) = shell.user %}<a href="/projects/{{ u }}">Projects</a><a href="/notifications">Notifications</a><a href="/settings">Settings</a>{% endif %} |
|
| 32 | + | <a href="/">{{ crate::web::icons::svg("home")|safe }}{% if shell.user.is_some() %}Dashboard{% else %}Repositories{% endif %}</a> |
|
| 33 | + | <a href="/explore">{{ crate::web::icons::svg("explore")|safe }}Explore</a> |
|
| 34 | + | <a href="/help">{{ crate::web::icons::svg("help")|safe }}Help</a> |
|
| 35 | + | {% if let Some(u) = shell.user %}<a href="/projects/{{ u }}">{{ crate::web::icons::svg("projects")|safe }}Projects</a><a href="/notifications">{{ crate::web::icons::svg("bell")|safe }}Notifications</a><a href="/settings">{{ crate::web::icons::svg("settings")|safe }}Settings</a>{% endif %} |
|
| 36 | 36 | <span class="grow"></span> |
|
| 37 | - | {% if shell.is_admin %}<a class="admin" href="/admin">Admin</a>{% endif %} |
|
| 37 | + | {% if shell.is_admin %}<a class="admin" href="/admin">{{ crate::web::icons::svg("admin")|safe }}Admin</a>{% endif %} |
|
| 38 | 38 | <a class="dim" href="/about" title="OxygenHub {{ shell.version }} ({{ shell.build }})">v{{ shell.version }}</a> |
|
| 39 | 39 | </nav> |
|
| 40 | 40 | {% block tabs %}{% endblock %} |
|
| … | 7 unchanged lines |
||
| 48 | 48 | <p class="small">{{ shell.site }} · a forge for Oxygen repositories. Nothing here is inferred: what a page says, the object store says.</p> |
|
| 49 | 49 | </div> |
|
| 50 | 50 | <div class="cols"> |
|
| 51 | - | <div><h3>Use</h3><a href="/help">Help</a><a href="/explore">Explore</a><a href="/api/v1/repos">API</a><a href="/.well-known/oxyhub-key">Hub key</a></div> |
|
| 51 | + | <div><h3>Use</h3><a href="/help">Help</a><a href="/explore">{{ crate::web::icons::svg("explore")|safe }}Explore</a><a href="/api/v1/repos">API</a><a href="/.well-known/oxyhub-key">Hub key</a></div> |
|
| 52 | 52 | <div><h3>Keys</h3><span class="small"><kbd>/</kbd> search · <kbd>g</kbd><kbd>c</kbd> code · <kbd>g</kbd><kbd>h</kbd> history · <kbd>?</kbd> help</span></div> |
|
| 53 | 53 | </div> |
|
| 54 | 54 | </div> |
|
| … | 9 unchanged lines |
modified templates/diffusion.html +2 −2
| … | 7 unchanged lines |
||
| 8 | 8 | <div class="head"><h1>Diffusion <small>how changes move between this repository and the ones that pin it</small></h1></div> |
|
| 9 | 9 | <p class="small mute df-key">Each pill has two halves. Left, the upstream: <span class="df-pill"><span class="df-half df-green">checked</span></span> its own run passed, <span class="df-pill"><span class="df-half df-amber">untested</span></span> it has no checks, <span class="df-pill"><span class="df-half df-red">failing</span></span> its run failed. Right, the repository that pins it: <span class="df-pill"><span class="df-half df-green">passes</span></span> its run on the bump passed, <span class="df-pill"><span class="df-half df-red">fails</span></span> it failed, <span class="df-pill"><span class="df-half df-grey">testing</span></span> still running, <span class="df-pill"><span class="df-half df-green">up to date</span></span> nothing to bump.</p> |
|
| 10 | 10 | {% if !used_by.is_empty() %} |
|
| 11 | - | <h2>Used by <span class="count-chip">{{ used_by.len() }}</span></h2> |
|
| 11 | + | <h2>Diffuses to <span class="count-chip">{{ used_by.len() }}</span></h2> |
|
| 12 | 12 | {% if unverified %}<div class="strip warn">{{ repo.owner }}/{{ repo.name }} has no checks of its own, so its consumers' bumps are amber: only their own pipelines test them. A <code>.flow/pipeline.kdl</code> here that passes turns them green.</div>{% endif %} |
|
| 13 | 13 | {% call d::rows(used_by) %} |
|
| 14 | 14 | {% endif %} |
|
| 15 | 15 | {% if !follows.is_empty() %} |
|
| 16 | - | <h2>Follows <span class="count-chip">{{ follows.len() }}</span></h2> |
|
| 16 | + | <h2>Takes from <span class="count-chip">{{ follows.len() }}</span></h2> |
|
| 17 | 17 | <div class="table"><table class="diffusion"> |
|
| 18 | 18 | <tr><th>Upstream</th><th>Subrepo</th><th>Behind</th><th>Upstream │ downstream</th><th>Open bump</th><th>Last landed</th></tr> |
|
| 19 | 19 | {% for (up, c) in follows %} |
|
| … | 14 unchanged lines |
modified templates/diffusion_rows.html +2 −1
| … | 1 unchanged lines |
||
| 2 | 2 | ||
| 3 | 3 | {% macro rows(list) %} |
|
| 4 | 4 | <div class="table"><table class="diffusion"> |
|
| 5 | + | <colgroup><col class="df-c-repo"><col class="df-c-pin"><col class="df-c-behind"><col class="df-c-pill"><col class="df-c-bump"><col class="df-c-landed"><col class="df-c-follow"></colgroup> |
|
| 5 | 6 | <tr><th>Repository</th><th>Pin on</th><th>Behind</th><th>Upstream │ downstream</th><th>Open bump</th><th>Last landed</th><th>Follows</th></tr> |
|
| 6 | 7 | {% for c in list %} |
|
| 7 | 8 | <tr> |
|
| 8 | - | <td><a href="/{{ c.owner }}/{{ c.name }}">{{ c.owner }}/{{ c.name }}</a> <span class="small mute">{{ c.path }}</span></td> |
|
| 9 | + | <td><a href="/{{ c.owner }}/{{ c.name }}">{{ c.owner }}/{{ c.name }}</a> <span class="small mute df-path">{{ c.path }}</span></td> |
|
| 9 | 10 | <td class="small"><span class="lbl lbl-bm">{{ c.bookmark }}</span></td> |
|
| 10 | 11 | <td class="small">{% match c.behind %}{% when Some(0) %}<span class="mute">0</span>{% when Some(n) %}{{ n }}{% when None %}<span class="mute" title="Its pin names changes this repository does not hold">?</span>{% endmatch %}</td> |
|
| 11 | 12 | <td>{% call chip(c) %}</td> |
|
| … | 7 unchanged lines |
modified templates/help.html +2 −0
| … | 121 unchanged lines |
||
| 122 | 122 | <tr><td>pull request</td><td><strong>proposal</strong></td><td>Reviewed and tested on its projected state. It cannot go stale, and approvals can be signed seals.</td></tr> |
|
| 123 | 123 | <tr><td>branch protection</td><td><strong>trust policy</strong></td><td>Signed rules in the repository itself: who may move which bookmark, with how many signatures. Every client enforces them, not only the hub.</td></tr> |
|
| 124 | 124 | <tr><td>Actions, CI badge</td><td><strong>OxygenFlow</strong>, state stamp</td><td>Jobs run in microVMs on exact states; results are receipts, and the stamp says which state they belong to.</td></tr> |
|
| 125 | + | <tr><td>git submodule</td><td><strong>subrepo</strong></td><td>Another Oxygen repository pinned at a path by its heads, listed in <code>.oxymodules</code>. The pin is a change like any other, so bumping it combines with your other work instead of conflicting with it.</td></tr> |
|
| 126 | + | <tr><td>Renovate, Dependabot</td><td><strong>Diffusion</strong></td><td>Built into the hub, not a bot you install. When a subrepo you pin moves on its default bookmark, the hub works out the bump, runs your checks on it and opens a proposal labelled <code>dependencies</code> with the upstream's changelog. Choose per repository under Settings › Diffusion (when the upstream is green, on every move, or off), or per subrepo in <code>.oxymodules</code>; it never lands anything for you. The Diffusion tab shows both directions: what you pin, and who pins you.</td></tr> |
|
| 125 | 127 | <tr><td>git blame</td><td><strong>blame</strong></td><td>Exact through rebases and squashes: each line is an atom with one authoring change, so there is no "ignore revisions" file.</td></tr> |
|
| 126 | 128 | <tr><td>fork</td><td><strong>fork</strong></td><td>A copy under your name with its own identity and bookmarks, sharing the stored objects. Its proposals go upstream as <code>you/name:bookmark</code>; the upstream's trust policy is not copied.</td></tr> |
|
| 127 | 129 | <tr><td>personal access token</td><td><strong>token</strong></td><td>For pushing and the API. Your signing key is what proves authorship; a token only authorises.</td></tr> |
|
| … | 7 unchanged lines |
modified templates/overview.html +35 −20
| … | 9 unchanged lines |
||
| 10 | 10 | {% if o.conflicted %}<div class="strip bad">This state carries conflicts. <a href="{{ repo.url }}/conflicts">See them</a></div>{% endif %} |
|
| 11 | 11 | <div class="bar"> |
|
| 12 | 12 | <span class="ref px"><a href="{{ repo.url }}/bookmarks">◆ {{ repo.default_bookmark }}</a></span> |
|
| 13 | - | <a class="go small" href="{{ repo.url }}/changes/{{ repo.default_bookmark }}">{{ o.changes|plural("change", "changes") }}</a> |
|
| 13 | + | {% if o.latest.is_none() %}<a class="go small" href="{{ repo.url }}/changes/{{ repo.default_bookmark }}">{{ o.changes|plural("change", "changes") }}</a>{% endif %} |
|
| 14 | 14 | {% if repo.can(crate::auth::Access::Write) %}<a class="go small" href="{{ repo.url }}/newfile/{{ repo.default_bookmark }}">new file</a>{% endif %} |
|
| 15 | 15 | <form class="find inline-find" method="get" action="{{ repo.url }}/search"><input type="search" name="q" placeholder="Search code…"></form> |
|
| 16 | 16 | <details class="clone-pop"> |
|
| … | 5 unchanged lines |
||
| 22 | 22 | </div> |
|
| 23 | 23 | </details> |
|
| 24 | 24 | </div> |
|
| 25 | - | {% if let Some(c) = o.latest %}<p class="small mute latest-line">latest <a href="{{ repo.url }}/change/{{ c.hash }}">{{ c.title }}</a> · {{ c.when_ms|ago }}</p>{% endif %} |
|
| 26 | 25 | {% if let Some(q) = quickstart %} |
|
| 27 | 26 | {% include "quickstart.html" %} |
|
| 28 | 27 | {% else %} |
|
| 28 | + | <div class="tree-box"> |
|
| 29 | + | {% if let Some(c) = o.latest %}<div class="tree-head"> |
|
| 30 | + | <span class="tree-title">{% if let Some(a) = c.author.account %}<a class="who" href="/{{ a }}"><img class="avatar" src="/avatar/{{ a }}" alt="" width="20" height="20"> {{ a }}</a>{% else %}<span class="who">{{ c.author.name }}</span>{% endif %} <a class="t" href="{{ repo.url }}/change/{{ c.hash }}" title="{{ c.title }}">{{ c.title }}</a></span> |
|
| 31 | + | <span class="tree-meta"><a href="{{ repo.url }}/change/{{ c.hash }}"><code>{{ c.short }}</code></a><time class="ago" title="{{ c.when_ms|when }}">{{ c.when_ms|ago }}</time><a class="hist" href="{{ repo.url }}/changes/{{ repo.default_bookmark }}">{{ crate::web::icons::svg("history")|safe }} {{ o.changes|plural("change", "changes") }}</a></span> |
|
| 32 | + | </div>{% endif %} |
|
| 29 | 33 | {% include "tree_table.html" %} |
|
| 34 | + | </div> |
|
| 30 | 35 | {% endif %} |
|
| 31 | 36 | {% if let Some(html) = readme_html %} |
|
| 32 | 37 | <article class="doc readme"><h2 class="small mute">{{ o.tree.readme.as_ref().unwrap().path }}</h2>{{ html|safe }}</article> |
|
| 33 | 38 | {% endif %} |
|
| 34 | 39 | </div> |
|
| 35 | - | <aside class="side"> |
|
| 36 | - | <h3>About</h3> |
|
| 40 | + | <aside class="side cards"> |
|
| 41 | + | <section class="side-card"><h3>About</h3><div class="body"> |
|
| 37 | 42 | {% if repo.description.is_empty() %}<p class="mute small">No description.</p>{% else %}<p>{{ repo.description }}</p>{% endif %} |
|
| 38 | 43 | {% if let Some(up) = fork.upstream %}<p class="small">forked from <a href="/{{ up.owner }}/{{ up.name }}">{{ up.owner }}/{{ up.name }}</a></p>{% endif %} |
|
| 44 | + | {% if !topics.is_empty() %}<p class="topics">{% for t in topics %}<a class="topic" href="/explore?topic={{ t }}">{{ t }}</a>{% endfor %}</p>{% endif %} |
|
| 39 | 45 | <div class="bar"> |
|
| 40 | - | {% if !topics.is_empty() %}<p class="topics">{% for t in topics %}<a class="topic" href="/explore?topic={{ t }}">{{ t }}</a>{% endfor %}</p>{% endif %} |
|
| 41 | 46 | <p class="small mute counts-line"><span title="stars">★ {{ stars }}</span> · <span title="watchers">👁 {{ watchers }}</span></p> |
|
| 42 | 47 | {% if shell.user.is_some() %}<form class="inline" method="post" action="{{ repo.url }}/star"><input type="hidden" name="_csrf" value="{{ shell.csrf }}"><button type="submit" class="small">{% if starred %}★ UNSTAR{% else %}☆ STAR{% endif %}</button></form> |
|
| 43 | 48 | <form class="inline" method="post" action="{{ repo.url }}/watch"><input type="hidden" name="_csrf" value="{{ shell.csrf }}"><button type="submit" class="small">{% if watching %}UNWATCH{% else %}WATCH{% endif %}</button></form> |
|
| … | 1 unchanged lines |
||
| 45 | 50 | {% if fork.forks > 0 %}<a class="small" href="{{ repo.url }}/forks">{{ fork.forks|plural("fork", "forks") }}</a>{% endif %} |
|
| 46 | 51 | </div> |
|
| 47 | 52 | {% if let Some(up) = fork.upstream %}{% if shell.user.is_some() %}<a class="go small" href="/{{ up.owner }}/{{ up.name }}/proposals/new?source={{ repo.owner }}/{{ repo.name }}:{{ repo.default_bookmark }}">propose to {{ up.owner }}/{{ up.name }}</a>{% endif %}{% endif %} |
|
| 53 | + | {% if let Some(l) = o.license %}<p class="small">Licence: <a href="{{ repo.url }}/file/{{ repo.default_bookmark }}/{{ l }}">{{ l }}</a></p>{% endif %} |
|
| 54 | + | </div></section> |
|
| 55 | + | {% if let Some(rel) = latest_release %}<section class="side-card"><h3>Latest release</h3><div class="body"><p><a href="{{ repo.url }}/releases/tag/{{ rel.tag }}">{{ rel.title }}</a> <span class="small mute">{{ rel.created_ms|ago_i }}</span></p></div></section>{% endif %} |
|
| 56 | + | {% if !follows.is_empty() || !used_by.is_empty() %} |
|
| 57 | + | <section class="side-card"><h3>Diffusion <a class="more" href="{{ repo.url }}/diffusion">details ›</a></h3><div class="body"> |
|
| 58 | + | {% if !follows.is_empty() %}<h4>Takes from</h4> |
|
| 59 | + | <ul class="plain df-list">{% for (up, c) in follows %}<li><a class="df-name" href="/{{ up.owner }}/{{ up.name }}" title="pinned at {{ c.path }}">{{ up.owner }}/{{ up.name }}</a> |
|
| 60 | + | <span class="df-line"><span class="df-pill" role="img" aria-label="upstream {{ c.upstream.word }}, here {{ c.downstream.word }}"><span class="df-half {{ c.upstream.class }}" title="Upstream: {{ c.upstream.title }}">{{ c.upstream.word }}</span><span class="df-half {{ c.downstream.class }}" title="Here: {{ c.downstream.title }}">{{ c.downstream.word }}</span></span>{% if let Some(n) = c.bump %}<a href="{{ repo.url }}/proposal/{{ n }}">bump #{{ n }}</a>{% endif %}</span></li>{% endfor %}</ul>{% endif %} |
|
| 61 | + | {% if !used_by.is_empty() %}<h4>Diffuses to <span class="count-chip">{{ used_by.len() }}</span></h4> |
|
| 62 | + | <ul class="plain df-list">{% for c in used_by %}<li><a class="df-name" href="/{{ c.owner }}/{{ c.name }}">{{ c.owner }}/{{ c.name }}</a> |
|
| 63 | + | <span class="df-line"><span class="df-pill" role="img" aria-label="here {{ c.upstream.word }}, {{ c.name }} {{ c.downstream.word }}"><span class="df-half {{ c.upstream.class }}" title="Here: {{ c.upstream.title }}">{{ c.upstream.word }}</span><span class="df-half {{ c.downstream.class }}" title="{{ c.owner }}/{{ c.name }}: {{ c.downstream.title }}">{{ c.downstream.word }}</span></span>{% match c.behind %}{% when Some(0) %}{% when Some(n) %}<span class="mute">{{ n }} behind</span>{% when None %}{% endmatch %}</span></li>{% endfor %}</ul>{% endif %} |
|
| 64 | + | </div></section> |
|
| 65 | + | {% endif %} |
|
| 48 | 66 | {% if !o.contributors.is_empty() %} |
|
| 49 | - | <h3>Contributors <span class="count-chip">{{ o.contributors.len() }}</span></h3> |
|
| 67 | + | <section class="side-card"><h3>Contributors <span class="count-chip">{{ o.contributors.len() }}</span></h3><div class="body"> |
|
| 50 | 68 | <ul class="plain contributors">{% for c in o.contributors.iter().take(10) %} |
|
| 51 | - | <li>{% if let Some(u) = c.account %}<a href="/{{ u }}"><img class="avatar sm" src="/avatar/{{ u }}" alt="" width="20" height="20"> {{ u }}</a> <span class="mute small">{{ c.name }}</span>{% else %}<span class="avatar-none" aria-hidden="true">○</span> {{ c.name }}{% endif %} <span class="mute small" title="changes">· {{ c.changes }}</span>{% if !c.via.is_empty() %}<ul class="plain via-list">{% for v in c.via %}<li><span class="mute" aria-hidden="true">↳</span> <span class="mute small">via</span> <img class="avatar sm" src="/avatar/agent/{{ v.0 }}" alt="" width="16" height="16"> {{ v.0 }} <span class="mute small" title="changes signed by this agent for them">· {{ v.1 }}</span></li>{% endfor %}</ul>{% endif %}</li> |
|
| 69 | + | <li class="c">{% if let Some(u) = c.account %}<img class="avatar sm" src="/avatar/{{ u }}" alt="" width="20" height="20"><a class="who" href="/{{ u }}" title="{{ c.name }}">{{ u }} <span class="mute small">{{ c.name }}</span></a>{% else if c.hub %}<img class="avatar sm" src="/favicon.svg" alt="" width="20" height="20"><span class="who" title="{{ c.name }}: this hub's delegate key, signing the changes made in the browser for people who allowed it">{{ c.name }}</span>{% else %}<span class="avatar-none" aria-hidden="true">○</span><span class="who" title="{{ c.name }}">{{ c.name }}</span>{% endif %}<span class="n mute small" title="changes">{{ c.changes }}</span>{% if !c.via.is_empty() %}<ul class="plain via-list">{% for v in c.via %}<li><span class="mute" aria-hidden="true">↳</span> <span class="mute small">via</span> <img class="avatar sm" src="/avatar/agent/{{ v.0 }}" alt="" width="16" height="16"> {{ v.0 }} <span class="mute small" title="changes signed by this agent for them">· {{ v.1 }}</span></li>{% endfor %}</ul>{% endif %}</li> |
|
| 52 | 70 | {% endfor %}</ul> |
|
| 53 | 71 | {% if o.contributors.len() > 10 %}<p class="small mute">and {{ o.contributors.len() - 10 }} more</p>{% endif %} |
|
| 72 | + | </div></section> |
|
| 54 | 73 | {% endif %} |
|
| 55 | 74 | {% if !o.languages.is_empty() %} |
|
| 56 | - | <h3>Languages</h3> |
|
| 75 | + | <section class="side-card"><h3>Languages</h3><div class="body"> |
|
| 57 | 76 | <div class="lang-bar" role="img" aria-label="{% for l in o.languages %}{{ l.name }} {{ "{:.1}"|format(l.percent) }}%{% if !loop.last %}, {% endif %}{% endfor %}">{% for l in o.languages %}<span style="width: {{ "{:.3}"|format(l.percent) }}%; background: {{ l.colour }}" title="{{ l.name }} {{ "{:.1}"|format(l.percent) }}%"></span>{% endfor %}</div> |
|
| 58 | 77 | <ul class="plain langs">{% for l in o.languages %}<li><span class="lang-dot" style="background: {{ l.colour }}" aria-hidden="true"></span> <strong>{{ l.name }}</strong> <span class="mute">{{ "{:.1}"|format(l.percent) }}%</span></li>{% endfor %}</ul> |
|
| 78 | + | </div></section> |
|
| 59 | 79 | {% endif %} |
|
| 60 | - | <h3>Bookmarks</h3> |
|
| 61 | - | <ul class="plain">{% for b in o.bookmarks %}<li><a href="{{ repo.url }}/tree/{{ b.name }}">{{ b.name }}</a>{% if b.heads.len() > 1 %} <span class="tag warn">{{ b.heads.len() }} heads</span>{% endif %}{% if b.conflicted %} <span class="tag bad">conflicts</span>{% endif %}</li>{% endfor %}</ul> |
|
| 62 | - | {% if let Some(rel) = latest_release %}<h3>Latest release</h3><p><a href="{{ repo.url }}/releases/tag/{{ rel.tag }}">{{ rel.title }}</a> <span class="small mute">{{ rel.created_ms|ago_i }}</span></p>{% endif %} |
|
| 63 | - | {% if !used_by.is_empty() %}<h3>Used by <span class="count-chip">{{ used_by.len() }}</span></h3> |
|
| 64 | - | <ul class="plain used-by">{% for c in used_by %}<li><a href="/{{ c.owner }}/{{ c.name }}">{{ c.owner }}/{{ c.name }}</a> <span class="df-pill"><span class="df-half {{ c.upstream.class }}" title="Upstream: {{ c.upstream.title }}">{{ c.upstream.word }}</span><span class="df-half {{ c.downstream.class }}" title="Downstream: {{ c.downstream.title }}">{% match c.behind %}{% when Some(0) %}up to date{% when Some(n) %}{{ c.downstream.word }} · {{ n }} behind{% when None %}{{ c.downstream.word }}{% endmatch %}</span></span></li>{% endfor %}</ul> |
|
| 65 | - | <p class="small"><a href="{{ repo.url }}/diffusion">Diffusion ›</a></p>{% endif %} |
|
| 66 | - | {% if !follows.is_empty() %}<h3>Follows</h3> |
|
| 67 | - | <ul class="plain used-by">{% for (up, c) in follows %}<li><a href="/{{ up.owner }}/{{ up.name }}">{{ up.owner }}/{{ up.name }}</a> <span class="df-pill" role="img" aria-label="upstream {{ c.upstream.word }}, downstream {{ c.downstream.word }}"><span class="df-half {{ c.upstream.class }}" title="Upstream: {{ c.upstream.title }}">{{ c.upstream.word }}</span><span class="df-half {{ c.downstream.class }}" title="Downstream: {{ c.downstream.title }}">{{ c.downstream.word }}</span></span>{% if let Some(n) = c.bump %} <a class="small" href="{{ repo.url }}/proposal/{{ n }}">#{{ n }}</a>{% endif %}</li>{% endfor %}</ul> |
|
| 68 | - | <p class="small"><a href="{{ repo.url }}/diffusion">Diffusion ›</a></p>{% endif %} |
|
| 69 | - | <h3>Tags</h3> |
|
| 80 | + | <section class="side-card"><h3>Bookmarks <span class="count-chip">{{ o.bookmarks.len() }}</span>{% if o.bookmarks.len() > 10 %}<a class="more" href="{{ repo.url }}/bookmarks">all {{ o.bookmarks.len() }} ›</a>{% endif %}</h3><div class="body"> |
|
| 81 | + | <ul class="plain bookmarks">{% for b in o.recent_bookmarks() %}<li><a href="{{ repo.url }}/tree/{{ b.name }}" title="{{ b.name }}">{{ b.name }}</a>{% if b.heads.len() > 1 %}<span class="tag warn">{{ b.heads.len() }} heads</span>{% endif %}{% if b.conflicted %}<span class="tag bad">conflicts</span>{% endif %}<time class="small mute" title="{{ b.latest_ms|when }}">{{ b.latest_ms|ago }}</time></li>{% endfor %}</ul> |
|
| 82 | + | </div></section> |
|
| 83 | + | <section class="side-card"><h3>Tags</h3><div class="body"> |
|
| 70 | 84 | {% if o.tags.is_empty() %}<p class="mute small">None.</p>{% else %}<ul class="plain">{% for t in o.tags %}<li><a href="{{ repo.url }}/tree/{{ t.name }}">{{ t.name }}</a>{% if t.signer.is_some() %} <span class="sig sig-valid" title="signed">✔</span>{% endif %}</li>{% endfor %}</ul>{% endif %} |
|
| 71 | - | <h3>Trust</h3> |
|
| 85 | + | </div></section> |
|
| 86 | + | <section class="side-card"><h3>Trust</h3><div class="body"> |
|
| 72 | 87 | {% match o.policy %} |
|
| 73 | 88 | {% when crate::repos::view::PolicyBadge::Active %}<p><a href="{{ repo.url }}/trust"><span class="sig sig-valid">✔</span> policy in force</a></p> |
|
| 74 | 89 | {% when crate::repos::view::PolicyBadge::Expired %}<p><a href="{{ repo.url }}/trust"><span class="sig sig-invalid">✘</span> policy expired</a></p> |
|
| 75 | 90 | {% when crate::repos::view::PolicyBadge::None %}<p><a href="{{ repo.url }}/trust"><span class="sig sig-unsigned">○</span> no policy</a></p> |
|
| 76 | 91 | {% endmatch %} |
|
| 77 | - | {% if let Some(l) = o.license %}<h3>Licence</h3><p><a href="{{ repo.url }}/file/{{ repo.default_bookmark }}/{{ l }}">{{ l }}</a></p>{% endif %} |
|
| 92 | + | </div></section> |
|
| 78 | 93 | </aside> |
|
| 79 | 94 | </div> |
|
| 80 | 95 | {% endblock %} |
modified templates/style.css +60 −2
| … | 143 unchanged lines |
||
| 144 | 144 | ||
| 145 | 145 | /* repository layout */ |
|
| 146 | 146 | .side-layout { display: grid; grid-template-columns: 1fr 280px; gap: 22px; } |
|
| 147 | + | .side-layout.rev { grid-template-columns: minmax(0, 1fr) 320px; } |
|
| 147 | 148 | /* A proposal's conversation: the page is wide for its diffs, but text and |
|
| 148 | 149 | the sidebar stay together at a readable width, and the description, |
|
| 149 | 150 | comments and forms fill their column. */ |
|
| … | 112 unchanged lines |
||
| 262 | 263 | ||
| 263 | 264 | @media (max-width: 760px) { |
|
| 264 | 265 | header { flex-wrap: wrap; } |
|
| 265 | - | .side-layout { grid-template-columns: 1fr; } |
|
| 266 | + | .side-layout, .side-layout.rev { grid-template-columns: 1fr; } |
|
| 266 | 267 | .foot { grid-template-columns: 1fr; } |
|
| 267 | 268 | main { padding: 10px 14px 20px; } |
|
| 268 | 269 | .band { padding: 0 8px; overflow-x: auto; } |
|
| … | 736 unchanged lines |
||
| 1005 | 1006 | .df-off { color: var(--dim); border-color: var(--line); border-style: dashed; } |
|
| 1006 | 1007 | .df-key .df-chip { margin-right: 2px; } |
|
| 1007 | 1008 | table.diffusion td { vertical-align: middle; } |
|
| 1009 | + | /* Every upstream's table shares one column grid (#152), so the tables on |
|
| 1010 | + | the Diffusion pages line up with each other. */ |
|
| 1011 | + | table.diffusion { table-layout: fixed; width: 100%; min-width: 880px; } |
|
| 1012 | + | table.diffusion td { overflow-wrap: anywhere; } |
|
| 1013 | + | table.diffusion .df-c-repo { width: 26%; } |
|
| 1014 | + | table.diffusion .df-c-pin { width: 10%; } |
|
| 1015 | + | table.diffusion .df-c-behind { width: 7%; } |
|
| 1016 | + | table.diffusion .df-c-pill { width: 17%; } |
|
| 1017 | + | table.diffusion .df-c-bump { width: 16%; } |
|
| 1018 | + | table.diffusion .df-c-landed { width: 14%; } |
|
| 1019 | + | table.diffusion .df-c-follow { width: 10%; } |
|
| 1020 | + | table.diffusion .df-path { display: block; } |
|
| 1008 | 1021 | ||
| 1009 | 1022 | /* The Diffusion pill (#150): two halves like a capsule, the upstream on |
|
| 1010 | 1023 | the left, the repository that pins it on the right. */ |
|
| … | 4 unchanged lines |
||
| 1015 | 1028 | .df-half.df-amber { background: #e3a008; color: #2b1d00; } |
|
| 1016 | 1029 | .df-half.df-red { background: #c62d47; color: #fff; } |
|
| 1017 | 1030 | .df-half.df-grey { background: var(--soft); color: var(--mute); } |
|
| 1018 | - | .df-half.df-off { background: transparent; color: var(--mute); } |
|
| 1031 | + | .df-half.df-off { background: transparent; color: var(--mute); border-style: solid; border-width: 0; } |
|
| 1032 | + | .df-half + .df-half.df-off { border-left: 1px solid var(--line); } |
|
| 1033 | + | ||
| 1034 | + | /* Icons (#152): inline SVGs in the colour of their text. */ |
|
| 1035 | + | svg.ic { width: 16px; height: 16px; vertical-align: -3px; flex: none; } |
|
| 1036 | + | .band a { display: inline-flex; align-items: center; gap: 6px; } |
|
| 1037 | + | .band a svg.ic { opacity: .8; } |
|
| 1038 | + | table.tree td.g svg.ic { vertical-align: -3px; } |
|
| 1039 | + | table.tree td.g.ic-dir { color: #54aeff; } |
|
| 1040 | + | table.tree td.g.ic-file, table.tree td.g.ic-link { color: var(--mute); } |
|
| 1041 | + | table.tree td.g.ic-sub { color: var(--acc); } |
|
| 1042 | + | ||
| 1043 | + | /* The tree's shaded head (#152): the latest change, and the history. */ |
|
| 1044 | + | .tree-box { margin-top: 4px; } |
|
| 1045 | + | .tree-head { display: flex; align-items: center; gap: 10px; background: var(--soft); border: 1px solid var(--line); border-bottom: 0; border-radius: 6px 6px 0 0; padding: 8px 16px; font-size: 13px; } |
|
| 1046 | + | .tree-head .tree-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } |
|
| 1047 | + | .tree-head .tree-title a.t { color: var(--ink); } |
|
| 1048 | + | .tree-head .tree-title a.who { font-weight: 600; color: var(--ink); } |
|
| 1049 | + | .tree-head img.avatar { width: 20px; height: 20px; border-radius: 50%; vertical-align: middle; } |
|
| 1050 | + | .tree-head .tree-meta { display: flex; align-items: center; gap: 12px; white-space: nowrap; color: var(--mute); } |
|
| 1051 | + | .tree-head .tree-meta a { color: var(--mute); } |
|
| 1052 | + | .tree-head .tree-meta a.hist { display: inline-flex; align-items: center; gap: 4px; color: var(--ink); font-weight: 600; } |
|
| 1053 | + | .tree-box .tree-head + table.tree { border-top-left-radius: 0; border-top-right-radius: 0; margin-top: 0; } |
|
| 1054 | + | @media (max-width: 760px) { .tree-head .tree-meta .ago { display: none; } } |
|
| 1055 | + | ||
| 1056 | + | /* The sidebar as cards (#152): each section a rounded box with a shaded |
|
| 1057 | + | heading. */ |
|
| 1058 | + | aside.side.cards { display: flex; flex-direction: column; gap: 14px; } |
|
| 1059 | + | .side-card { border: 1px solid var(--line); border-radius: 6px; background: var(--panel); overflow: hidden; } |
|
| 1060 | + | .side-card > h3 { margin: 0; padding: 8px 12px; background: var(--soft); border-bottom: 1px solid var(--line); display: flex; align-items: center; gap: 6px; } |
|
| 1061 | + | .side-card > h3 .more { margin-left: auto; font-size: 12px; font-weight: 400; text-transform: none; letter-spacing: 0; } |
|
| 1062 | + | .side-card > .body { padding: 10px 12px; } |
|
| 1063 | + | .side-card > .body > :first-child { margin-top: 0; } |
|
| 1064 | + | .side-card > .body > :last-child { margin-bottom: 0; } |
|
| 1065 | + | .side-card h4 { margin: 10px 0 4px; font-size: 12px; font-weight: 600; color: var(--mute); } |
|
| 1066 | + | .side-card h4:first-child { margin-top: 0; } |
|
| 1067 | + | ul.contributors li.c { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; } |
|
| 1068 | + | ul.contributors li.c > .who { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } |
|
| 1069 | + | ul.contributors li.c > .n { margin-left: auto; } |
|
| 1070 | + | ul.df-list { margin: 0; } |
|
| 1071 | + | ul.df-list li { padding: 6px 0; border-top: 1px solid var(--line); } |
|
| 1072 | + | ul.df-list li:first-child { border-top: 0; padding-top: 0; } |
|
| 1073 | + | ul.df-list .df-name { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } |
|
| 1074 | + | ul.df-list .df-line { display: flex; align-items: center; gap: 8px; margin-top: 3px; font-size: 12px; } |
|
| 1075 | + | ul.bookmarks li { display: flex; align-items: center; gap: 6px; } |
|
| 1076 | + | ul.bookmarks li > a { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } |
modified templates/tree_rows.html +2 −2
| … | 2 unchanged lines |
||
| 3 | 3 | {% else %} |
|
| 4 | 4 | <table class="tree" data-last="{{ repo.url }}/last/{{ t.target.label }}" data-change="{{ repo.url }}/change/"> |
|
| 5 | 5 | {% if !t.path.is_empty() %} |
|
| 6 | - | <tr><td class="g">↰</td><td colspan="3"><a href="{{ repo.url }}/tree/{{ t.target.label }}{% if let Some((_, p)) = t.crumbs.iter().rev().nth(1) %}/{{ p }}{% endif %}">..</a></td></tr> |
|
| 6 | + | <tr><td class="g ic-file">{{ crate::web::icons::svg("up")|safe }}</td><td colspan="3"><a href="{{ repo.url }}/tree/{{ t.target.label }}{% if let Some((_, p)) = t.crumbs.iter().rev().nth(1) %}/{{ p }}{% endif %}">..</a></td></tr> |
|
| 7 | 7 | {% endif %} |
|
| 8 | 8 | {% for e in t.entries %} |
|
| 9 | 9 | <tr data-path="{{ e.path }}"> |
|
| 10 | - | <td class="g">{{ e.kind.glyph() }}</td> |
|
| 10 | + | <td class="g {{ e.kind.class() }}">{{ e.kind.icon()|safe }}</td> |
|
| 11 | 11 | <td class="name"> |
|
| 12 | 12 | {% if e.kind == crate::repos::view::EntryKind::Dir %}<a href="{{ repo.url }}/tree/{{ t.target.label }}/{{ e.path }}">{{ e.name }}</a> |
|
| 13 | 13 | {% else %}<a href="{{ repo.url }}/file/{{ t.target.label }}/{{ e.path }}">{{ e.name }}</a>{% endif %} |
|
| … | 9 unchanged lines |