Iterate on layout, design, outlines and contextual elements

This commit is contained in:
Simon Repp
2025-08-06 11:00:13 +02:00
parent 9db27973dd
commit 8e7fc3c237
6 changed files with 142 additions and 121 deletions
+35 -43
View File
@@ -14,6 +14,7 @@ a:hover { color: var(--fg-3); }
cursor: pointer;
display: flex;
padding: .3rem .6rem .3rem .5rem;
white-space: nowrap;
}
.actions a:focus-visible,
.actions a:hover,
@@ -62,6 +63,17 @@ button {
text-underline-offset: .3rem;
}
.center .description a:hover { text-decoration: none; }
.context_dot {
border-radius: 50%;
display: block;
height: 1rem;
width: 1rem;
}
.context_dot:focus-visible,
.context_dot:hover {
box-shadow: 0 0 .15rem var(--mg);
}
.context_dot + .context_dot { margin-left: .3rem; }
#copy-link::after {
color: var(--fg-2);
padding-left: .5rem;
@@ -93,12 +105,13 @@ button {
}
.flex { display: flex; }
.flex.align { align-items: center; }
.flex.align_start { align-items: flex-start; }
.flex.apart { justify-content: space-between; }
.flex.space { column-gap: .3rem; }
.flex.wrap { flex-wrap: wrap; }
footer {
align-items: center;
background: var(--bg-1);
box-sizing: border-box;
display: flex;
font-size: .9rem;
flex-wrap: wrap;
@@ -191,34 +204,25 @@ img {
padding: .5rem;
}
.medium_font { font-size: 1.2rem; }
/*.outline {
.parent_context {
display: flex;
flex-wrap: wrap;
font-size: .9rem;
gap: .3rem;
margin-bottom: .2rem;
}
.parent_context a:not(.context_dot) {
align-items: center;
column-gap: .3rem;
display: flex;
}
.parent_context > div {
align-items: flex-end;
display: flex;
flex-direction: column;
row-gap: 1rem;
gap: .3rem;
}
.outline > a {
border: 1px solid var(--bg-2);
border-radius: .6rem;
position: relative;
}
.outline > a > img,
.outline > a > .placeholder {
border-radius: .6rem;
}
.outline > a > .placeholder {
background: var(--bg-2);
height: 12rem;
}
.outline .text {
background: var(--bg-1-85);
border-bottom-left-radius: .6rem;
border-bottom-right-radius: .6rem;
bottom: 0;
left: 0;
padding: 1rem;
position: absolute;
right: 0;
}*/
.parent_context > div > div:nth-child(2) { display: flex; }
.parents {
font-size: .9rem;
margin-top: .2rem;
@@ -346,12 +350,7 @@ img {
@media (max-aspect-ratio: 2/3) {
.player_wrapper:not(:fullscreen) video { max-height: 76dvh; }
}
.playlist_context {
border: 1px solid var(--bg-2);
border-radius: .5rem;
/* TODO: Shadows only in light mode or different strategy between both color schemes */
box-shadow: 0 0 12rem #0001;
}
.playlist_context { margin-top: 6.1rem; }
.playlist_context > a {
display: flex;
outline: none;
@@ -370,7 +369,7 @@ img {
.playlist_context > a:focus-visible,
.playlist_context > a:hover { background: var(--bg-2); }
.playlist_context > a:not(:last-child) {
border-bottom: 1px solid var(--bg-1);
margin-bottom: .5rem;
}
.playlist_context .current { background: var(--bg-3); }
.playlist_context .description {
@@ -416,15 +415,6 @@ img {
height: 4rem;
object-fit: contain;
}
.playlist_header {
background: var(--bg-1);
padding: .8rem;
}
.playlist_header .duration { color: var(--fg-3); }
.playlist_header .title {
color: var(--fg-1);
display: block;
}
.playlist_page {
display: flex;
flex-direction: column;
@@ -667,15 +657,17 @@ svg {
}
.versions_ring.interacting g:not(:focus-visible):not(:hover) { opacity: .5; }
video {
background: var(--d-1);
background: #000;
border-radius: .5rem;
display: block;
height: 100%;
width: 100%;
}
.video_info { margin-bottom: 1.5rem; }
.video_title {
color: var(--fg-1);
margin: 0;
margin-bottom: .2em;
}
.videos {
column-gap: 1rem;
-15
View File
@@ -134,21 +134,6 @@ pub fn collection_html(
body.push_str(&rendered);
}
// if traversal.steps.len() > 1 {
// let outline_items = traversal.steps
// .iter()
// .filter(|step| !step.is_collection(collection))
// .enumerate()
// .map(|(index, step)| {
// let prefix = "../".repeat(traversal.steps.len() - 1 - index);
// step.context_for_collection(collection, &prefix, site)
// })
// .collect::<Vec<String>>()
// .join("\n");
// body.push_str(&format!(r#"<div class="outline">{outline_items}</div>"#));
// }
if collection.platform_integration().enabled() {
if let Some(collection_url) = site.absolute_url_index(&collection.site_path) {
let mut meta = PlatformIntegrationMeta::new(title.to_string(), collection_url);
+81 -30
View File
@@ -24,8 +24,6 @@ pub struct Breadcrumb<'a> {
pub step: &'a TraversalStep<'a>
}
// TODO: Consider the similarity to TraversalStep here (!) This indicates
// we should really have a generic item of that sort for representing both?
pub enum CurrentPage<'a> {
Collection(&'a Collection),
Playlist(&'a Playlist),
@@ -438,6 +436,72 @@ impl TraversalStep<'_> {
}
}
pub fn context_with_bubbles(
&self,
prefix: &str,
site: &Site
) -> String {
let index_suffix_file_only = site.index_suffix_file_only();
let translations = &site.language.translations;
match self {
TraversalStep::Collection(collection) => {
let label = collection.title_or_slug_or_generic_label(translations);
let label_escaped = html_escape_outside_attribute(label);
let video_bubbles = collection.public_videos()
.map(|video| {
if let Some(poster_meta) = &video.poster.as_ref().and_then(|poster| poster.poster_meta.as_ref()) {
let background = poster_meta.lqip.gradient_radial();
let video_slug = video.slug_unchecked();
formatdoc!(r#"<a class="context_dot" href="{prefix}{video_slug}/" style="{background}"></a>"#)
} else {
String::new()
}
})
.collect::<Vec<String>>()
.join("");
let count = collection.public_videos().count();
format!(r#"
<div>
<div><a href="{prefix}{index_suffix_file_only}">{label_escaped}</a></div>
<div style="--count: {count}">{video_bubbles}</div>
</div>
"#)
}
TraversalStep::Playlist(playlist) => {
let label = playlist.title_or_slug_or_generic_label(translations);
let label_escaped = html_escape_outside_attribute(label);
let video_bubbles = playlist.public_videos()
.map(|video| {
if let Some(poster_meta) = &video.poster.as_ref().and_then(|poster| poster.poster_meta.as_ref()) {
let background = poster_meta.lqip.gradient_radial();
let video_slug = video.slug_unchecked();
formatdoc!(r#"<a class="context_dot" href="{prefix}{video_slug}/" style="{background}"></a>"#)
} else {
String::new()
}
})
.collect::<Vec<String>>()
.join("");
let count = playlist.public_videos().count();
format!(r#"
<div>
<div><a href="{prefix}{index_suffix_file_only}">{label_escaped}</a></div>
<div style="--count: {count}">{video_bubbles}</div>
</div>
"#)
}
TraversalStep::Video(_) => unimplemented!(),
TraversalStep::Virtual { .. } => unimplemented!()
}
}
fn crumb_collection_header(
collection: &Collection,
prefix: &str,
@@ -655,27 +719,19 @@ impl TraversalStep<'_> {
site: &Site,
video: Option<&Video>
) -> String {
let banner = if let Some(banner) = &playlist.banner {
banner_html(banner, prefix)
} else if let Some(video) = &playlist.public_videos()
.find(|video| video.poster.is_some()) {
let video_prefix = format!("{prefix}{}/", video.slug_unchecked());
poster_html(video.poster.as_ref().unwrap(), &video_prefix)
} else {
String::from(r#"<div class="placeholder"></div>"#)
};
let label = playlist.title_or_slug_or_generic_label(&site.language.translations);
let outline_stats = OutlineStats::for_playlist(playlist).render(site);
let translations = &site.language.translations;
let header_crumb = formatdoc!(r#"
<a href="{prefix}">
{banner}
<div class="text">
<div class="medium_font">{label}</div>
<small>{outline_stats}</small>
</div>
</a>
"#);
// let banner = if let Some(banner) = &playlist.banner {
// banner_html(banner, prefix)
// } else if let Some(video) = &playlist.public_videos()
// .find(|video| video.poster.is_some()) {
// let video_prefix = format!("{prefix}{}/", video.slug_unchecked());
// poster_html(video.poster.as_ref().unwrap(), &video_prefix)
// } else {
// String::from(r#"<div class="placeholder"></div>"#)
// };
let label = playlist.title_or_slug_or_generic_label(translations);
if let Some(video) = video {
let index_suffix = site.index_suffix();
@@ -686,7 +742,7 @@ impl TraversalStep<'_> {
.iter()
.enumerate()
.map(|(index, video_iterated)| {
let label = video_iterated.title_or_slug_or_generic_label(&site.language.translations);
let label = video_iterated.title_or_slug_or_generic_label(translations);
let slug = video_iterated.slug_unchecked();
let video_prefix = if video_iterated.site_path == video.site_path {
@@ -725,18 +781,13 @@ impl TraversalStep<'_> {
.collect::<Vec<String>>()
.join("\n");
let videos_crumb = formatdoc!(r#"
formatdoc!(r#"
<div class="playlist_context">
{items}
</div>
"#);
formatdoc!(r#"
{header_crumb}
{videos_crumb}
"#)
} else {
header_crumb
String::new()
}
}
-15
View File
@@ -120,21 +120,6 @@ pub fn playlist_html(
body.push_str(&videos_rendered);
}
// if !traversal.steps.is_empty() {
// let outline_items = traversal.steps
// .iter()
// .filter(|step| !step.is_playlist())
// .enumerate()
// .map(|(index, step)| {
// let prefix = "../".repeat(traversal.steps.len() - 1 - index);
// step.context_for_playlist(playlist, &prefix, site)
// })
// .collect::<Vec<String>>()
// .join("\n");
// body.push_str(&format!(r#"<div class="outline">{outline_items}</div>"#));
// }
if playlist.platform_integration().enabled() {
if let Some(playlist_url) = site.absolute_url_index(&playlist.site_path) {
let mut meta = PlatformIntegrationMeta::new(title.to_string(), playlist_url);
+19 -18
View File
@@ -85,22 +85,18 @@ pub fn video_html(
}
};
// let outline_items = traversal.steps
// .iter()
// .filter(|step| !step.is_video(video))
// .enumerate()
// .map(|(index, step)| {
// let prefix = "../".repeat(traversal.steps.len() - 1 - index);
// step.context_for_video(&prefix, site, video)
// })
// .collect::<Vec<String>>()
// .join("\n");
let parent_context = if traversal.steps.len() > 1 {
let parent_step = &traversal.steps[traversal.steps.len() - 2];
let context_with_bubbles = parent_step.context_with_bubbles("../", site);
// let outline_rendered = formatdoc!(r#"
// <div class="outline">
// {outline_items}
// </div>
// "#);
formatdoc!(r#"
<div class="parent_context">
{context_with_bubbles}
</div>
"#)
} else {
String::new()
};
let video_prefix = "";
let player_rendered = player(false, next_video, site, video, video_prefix);
@@ -152,11 +148,16 @@ pub fn video_html(
let video_content = formatdoc!(r#"
<div>
<div class="video_info">
<div class="align apart flex space wrap" style="margin-top: 2rem;">
<h1 class="video_title">{e_title} {r_unlisted_badge}</h1>
{parent_context}
</div>
</div>
{player_rendered}
<div class="video_info">
<div class="align apart flex" style="margin-top: 2rem;">
<div class="align_start apart flex wrap" style="margin-top: 2rem;">
<div>
<h1 class="video_title">{e_title} {r_unlisted_badge}</h1>
{release_date}
</div>
<div class="actions align flex space">
@@ -165,8 +166,8 @@ pub fn video_html(
{r_downloads}
</div>
</div>
{description}
{links_rendered}
{description}
</div>
</div>
"#);
+7
View File
@@ -532,6 +532,13 @@ impl Site {
}
}
pub fn index_suffix_file_only(&self) -> &str {
match self.clean_urls {
true => "",
false => "index.html"
}
}
pub fn new() -> Site {
Site {
base_url: None,