Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ To analyze the state of your dependencies you can use the following URLs:

On the analysis page, you will also find the markdown code to include a fancy badge in your project README so visitors (and you) can see at a glance if your dependencies are still up to date!

For crates on crates.io, badge URLs support both pinned versions and latest-release shortcuts:
- latest crates.io release:
- `https://deps.rs/crate/<NAME>/latest/status.svg`
- specific crates.io release:
- `https://deps.rs/crate/<NAME>/<VERSION>/status.svg`
For crates on crates.io, you can use either:
- latest release: `https://deps.rs/crate/<NAME>/latest/status.svg`
- pinned version: `https://deps.rs/crate/<NAME>/<VERSION>/status.svg`

Use `latest` for a moving badge, or `<VERSION>` for a fixed release.

Badges have a few options, specified with query parameters:
- `style`: which matches the styles from `shields.io`:
Expand Down
74 changes: 74 additions & 0 deletions assets/links.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,77 @@ function buildCrateLink() {

return false;
}

function activateBadgeTab(root, target) {
let tabs = root.querySelectorAll("[data-badge-target]");
let panels = root.querySelectorAll("[data-badge-panel]");

tabs.forEach(function(tab) {
let li = tab.closest("li");
let isActive = tab.dataset.badgeTarget === target;

if (!li) {
return;
}

li.classList.toggle("is-active", isActive);
tab.setAttribute("aria-selected", isActive ? "true" : "false");
tab.tabIndex = isActive ? 0 : -1;
});

panels.forEach(function(panel) {
panel.hidden = panel.dataset.badgePanel !== target;
});
}

document.addEventListener("DOMContentLoaded", function() {
document.querySelectorAll("[data-badge-root]").forEach(function(root) {
let container = root.querySelector("[data-badge-tabs]");
if (!container) {
return;
}

let tabs = Array.from(container.querySelectorAll("[data-badge-target]"));
let activeTab =
tabs.find(function(tab) {
return tab.getAttribute("aria-selected") === "true";
}) || tabs[0];
if (activeTab) {
activateBadgeTab(root, activeTab.dataset.badgeTarget);
}

tabs.forEach(function(tab) {
tab.addEventListener("click", function() {
activateBadgeTab(root, tab.dataset.badgeTarget);
});

tab.addEventListener("keydown", function(event) {
let key = event.key;
if (key !== "ArrowRight" && key !== "ArrowLeft" && key !== "Home" && key !== "End") {
return;
}

event.preventDefault();
let currentIndex = tabs.indexOf(tab);
let nextIndex = currentIndex;
if (key === "ArrowRight") {
nextIndex = (currentIndex + 1) % tabs.length;
} else if (key === "ArrowLeft") {
nextIndex = (currentIndex - 1 + tabs.length) % tabs.length;
} else if (key === "Home") {
nextIndex = 0;
} else if (key === "End") {
nextIndex = tabs.length - 1;
}

let nextTab = tabs[nextIndex];
if (!nextTab) {
return;
}

activateBadgeTab(root, nextTab.dataset.badgeTarget);
nextTab.focus();
});
});
});
});
83 changes: 83 additions & 0 deletions assets/styles/_badge-tabs.sass
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Badge tabs integrated with standard Bulma pre blocks
.badge-group
display: flex
flex-direction: column
position: relative
padding-top: 1.65rem // Reserve space for tabs to prevent height jumping
// No background or border on the wrapper itself

.tabs
position: absolute
top: 0
right: 0
border-bottom: none
flex-shrink: 0
margin-bottom: 0
padding: 0
width: auto
z-index: 1 // Sit above the pre block

ul
border-bottom: none
justify-content: flex-end
gap: 1px

li
margin: 0

button
align-items: center
// Inactive state: slightly darker than the pre background to look "behind"
background-color: darken($background, 5%)
border: 1px solid transparent
border-bottom: none
border-radius: $radius $radius 0 0
color: $text-light
cursor: pointer
display: flex
font-size: 0.75rem
font-weight: $weight-medium
justify-content: center
line-height: 1
margin-bottom: 0
padding: 0.4rem 0.75rem
position: relative
transition: background-color 0.2s, color 0.2s
user-select: none

&:hover
background-color: darken($background, 2%)
color: $text-strong

// Active state: matches the pre block background ($background / $white-ter)
&[aria-selected="true"]
background-color: $background // Match Bulma pre background
color: $text-strong
// Create a seamless connection
padding-bottom: calc(0.4rem + 1px)
margin-bottom: -1px

&::after
// The "masking tape" to hide any sub-pixel gap
content: ""
display: block
position: absolute
bottom: -1px
left: 0
right: 0
height: 2px
background-color: $background // Match pre background
z-index: 10

button:focus-visible
outline: 2px solid $link
outline-offset: -2px

// The code block (pre)
// We target the specific pre inside our group to apply necessary tweaks
.badge-code
border-top-right-radius: 0 // Flatten top-right to attach tab
position: relative
z-index: 0
margin-top: 0
padding: 0.5rem 1rem // More compact padding to reduce height
2 changes: 2 additions & 0 deletions assets/styles/main.sass
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ $family-monospace: "Source Serif Pro", monospace
@import "bulma/components/level"
@import "bulma/components/message"
@import "bulma/components/navbar"
@import "bulma/components/tabs"

@import "bulma/grid/columns"
@import "bulma/layout/_all"
@import "./badge-tabs"
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ async fn main() {
.app_data(ThinData(engine.clone()))
.service(server::index)
.service(server::crate_redirect)
.service(server::crate_latest_status_html)
.service(server::crate_latest_status_svg)
.service(server::crate_latest_status_shield_json)
.service(server::crate_status_svg)
Expand Down
64 changes: 62 additions & 2 deletions src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ enum StatusFormat {
ShieldJson,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum BadgeTabMode {
Hidden,
PinnedDefault,
LatestDefault,
}

#[get("/")]
pub(crate) async fn index(ThinData(engine): ThinData<Engine>) -> actix_web::Result<impl Responder> {
let popular = future::try_join(engine.get_popular_repos(), engine.get_popular_crates()).await;
Expand Down Expand Up @@ -116,8 +123,13 @@ async fn repo_status(
match analyze_result {
Err(err) => {
tracing::error!(%err);
let response =
status_format_analysis(None, format, SubjectPath::Repo(repo_path), extra_knobs);
let response = status_format_analysis(
None,
format,
SubjectPath::Repo(repo_path),
extra_knobs,
BadgeTabMode::Hidden,
);

Ok(response)
}
Expand All @@ -128,6 +140,7 @@ async fn repo_status(
format,
SubjectPath::Repo(repo_path),
extra_knobs,
BadgeTabMode::Hidden,
);

Ok(response)
Expand Down Expand Up @@ -180,6 +193,15 @@ async fn crate_status_html(
crate_status(engine, uri, (name, Some(version)), StatusFormat::Html).await
}

#[get("/crate/{name}/latest")]
async fn crate_latest_status_html(
ThinData(engine): ThinData<Engine>,
uri: Uri,
Path((name,)): Path<(String,)>,
) -> actix_web::Result<impl Responder> {
crate_status(engine, uri, (name, None), StatusFormat::Html).await
}

#[get("/crate/{name}/latest/status.svg")]
async fn crate_latest_status_svg(
ThinData(engine): ThinData<Engine>,
Expand Down Expand Up @@ -222,6 +244,8 @@ async fn crate_status(
(name, version): (String, Option<String>),
format: StatusFormat,
) -> actix_web::Result<impl Responder> {
let is_latest_crate_route = version.is_none();

let version = match version {
Some(ver) => ver.to_owned(),
None => {
Expand Down Expand Up @@ -256,6 +280,9 @@ async fn crate_status(
}

Ok(crate_path) => {
let badge_tab_mode =
resolve_badge_tab_mode(&engine, format, &crate_path, is_latest_crate_route).await;

let analysis_outcome = engine
.analyze_crate_dependencies(crate_path.clone())
.await
Expand All @@ -269,18 +296,50 @@ async fn crate_status(
format,
SubjectPath::Crate(crate_path),
badge_knobs,
badge_tab_mode,
);

Ok(response)
}
}
}

async fn resolve_badge_tab_mode(
engine: &Engine,
format: StatusFormat,
crate_path: &CratePath,
is_latest_crate_route: bool,
) -> BadgeTabMode {
if format != StatusFormat::Html {
return BadgeTabMode::Hidden;
}

if is_latest_crate_route {
return BadgeTabMode::LatestDefault;
}

let latest_release = engine
.find_latest_stable_crate_release(crate_path.name.clone(), VersionReq::STAR)
.await;

match latest_release {
Ok(Some(latest_rel)) if latest_rel.version == crate_path.version => {
BadgeTabMode::PinnedDefault
}
Ok(Some(_)) | Ok(None) => BadgeTabMode::Hidden,
Err(err) => {
tracing::error!(%err);
BadgeTabMode::Hidden
}
}
}

fn status_format_analysis(
analysis_outcome: Option<AnalyzeDependenciesOutcome>,
format: StatusFormat,
subject_path: SubjectPath,
badge_knobs: ExtraConfig,
badge_tab_mode: BadgeTabMode,
) -> impl Responder {
match format {
StatusFormat::Svg => Either::Left(views::badge::response(
Expand All @@ -292,6 +351,7 @@ fn status_format_analysis(
analysis_outcome,
subject_path,
badge_knobs,
badge_tab_mode,
)),
StatusFormat::ShieldJson => Either::Left(views::badge::shield_json_response(
analysis_outcome.as_ref(),
Expand Down
Loading