diff --git a/src/build/assets/navigation.js b/src/build/assets/navigation.js index 9009868..d5079da 100644 --- a/src/build/assets/navigation.js +++ b/src/build/assets/navigation.js @@ -1,3 +1,5 @@ +const chevronRightIcon = document.querySelector('#chevron_right_icon').content; + const browseButton = document.querySelector('button.browse'); const navigation = document.querySelector('header .navigation'); @@ -70,6 +72,10 @@ function initializeSiteTree() { if (video.image) { image = document.createElement('img'); image.src = rootPrefix + video.sitePath + video.image; + + if (video.lqip) { + image.style.background = lqipToRadialGradient(video.lqip); + } } else { image = document.createElement('span'); image.classList.add('placeholder'); @@ -92,10 +98,24 @@ function initializeSiteTree() { function initializePlaylist(playlist, ancestors = undefined) { const traversal = ancestors ? [...ancestors, playlist] : []; + playlist.expandToggle = document.createElement('span'); + const spanTitle = document.createElement('span'); - spanTitle.textContent = ancestors - ? traversal.map(ancestor => ancestor.title).join(' > ') - : playlist.title; + if (ancestors) { + for (const [index, ancestor] of traversal.entries()) { + if (index > 0) { + const separator = document.createElement('span'); + separator.appendChild(chevronRightIcon.cloneNode(true)); + separator.setAttribute('aria-label', '>'); + + spanTitle.appendChild(separator); + } + + spanTitle.append(ancestor.title); + } + } else { + spanTitle.textContent = playlist.title; + } const spanInfo = document.createElement('span'); spanInfo.textContent = NAVIGATION_JS_T.xxxVideos(playlist.videos.length); @@ -103,9 +123,20 @@ function initializeSiteTree() { playlist.element = document.createElement('a'); playlist.element.classList.add('playlist'); playlist.element.href = rootPrefix + playlist.sitePath + indexSuffix; + playlist.element.appendChild(playlist.expandToggle); playlist.element.appendChild(spanTitle); playlist.element.appendChild(spanInfo); + if (playlist.videos.length) { + playlist.expandToggle.appendChild(chevronRightIcon.cloneNode(true)); + playlist.expandToggle.setAttribute('aria-label', NAVIGATION_JS_T.expand); + playlist.expandToggle.setAttribute('title', NAVIGATION_JS_T.expand); + playlist.expandToggle.addEventListener('click', event => { + collapseExpand(playlist.element); + event.preventDefault(); + }); + } + siteTreeElements.appendChild(playlist.element); for (const video of playlist.videos) { @@ -118,20 +149,44 @@ function initializeSiteTree() { function initializeCollection(collection, ancestors = undefined) { const traversal = ancestors ? [...ancestors, collection] : []; - const spanText = document.createElement('span'); - spanText.textContent = ancestors - ? traversal.map(ancestor => ancestor.title).join(' > ') - : collection.title; + collection.expandToggle = document.createElement('span'); + + const spanTitle = document.createElement('span'); + if (ancestors) { + for (const [index, ancestor] of traversal.entries()) { + if (index > 0) { + const separator = document.createElement('span'); + separator.appendChild(chevronRightIcon.cloneNode(true)); + separator.setAttribute('aria-label', '>'); + + spanTitle.appendChild(separator); + } + + spanTitle.append(ancestor.title); + } + } else { + spanTitle.textContent = collection.title; + } collection.element = document.createElement('a'); collection.element.classList.add('collection'); collection.element.href = rootPrefix + collection.sitePath + indexSuffix; - collection.element.appendChild(spanText); + collection.element.appendChild(collection.expandToggle); + + collection.element.appendChild(spanTitle); if (collection.videos.length) { const spanInfo = document.createElement('span'); spanInfo.textContent = NAVIGATION_JS_T.xxxVideos(collection.videos.length); collection.element.appendChild(spanInfo); + + collection.expandToggle.appendChild(chevronRightIcon.cloneNode(true)); + collection.expandToggle.setAttribute('aria-label', NAVIGATION_JS_T.expand); + collection.expandToggle.setAttribute('title', NAVIGATION_JS_T.expand); + collection.expandToggle.addEventListener('click', event => { + collapseExpand(collection.element); + event.preventDefault(); + }); } siteTreeElements.appendChild(collection.element); @@ -158,15 +213,13 @@ function initializeSiteTree() { siteTreeInitialized = true; } -// TODO: Currently unused but kept around in case we want to pick it up again -// (the lqip property is available as video.lqip in the siteContent tree structure). -// function lqipToRadialGradient(lqip) { -// const [ne, nw, se, sw] = lqip; -// return `radial-gradient(ellipse at top right, rgb(${ne[0]},${ne[1]},${ne[2]}), transparent),` + -// `radial-gradient(ellipse at top left, rgb(${nw[0]},${nw[1]},${nw[2]}), transparent),` + -// `radial-gradient(ellipse at bottom right, rgb(${se[0]},${se[1]},${se[2]}), transparent),` + -// `radial-gradient(ellipse at bottom left, rgb(${sw[0]},${sw[1]},${sw[2]}), transparent)`; -// } +function lqipToRadialGradient(lqip) { + const [ne, nw, se, sw] = lqip; + return `radial-gradient(ellipse at top right, rgb(${ne[0]},${ne[1]},${ne[2]}), transparent),` + + `radial-gradient(ellipse at top left, rgb(${nw[0]},${nw[1]},${nw[2]}), transparent),` + + `radial-gradient(ellipse at bottom right, rgb(${se[0]},${se[1]},${se[2]}), transparent),` + + `radial-gradient(ellipse at bottom left, rgb(${sw[0]},${sw[1]},${sw[2]}), transparent)`; +} function toggleBrowse() { function updateVideo(video) { @@ -179,6 +232,9 @@ function toggleBrowse() { } playlist.element.classList.add('visible'); + playlist.element.classList.remove('expanded'); + playlist.expandToggle.setAttribute('aria-label', NAVIGATION_JS_T.expand); + playlist.expandToggle.setAttribute('title', NAVIGATION_JS_T.expand); } function updateCollection(collection) { @@ -195,6 +251,9 @@ function toggleBrowse() { } collection.element.classList.add('visible'); + collection.element.classList.remove('expanded'); + collection.expandToggle.setAttribute('aria-label', NAVIGATION_JS_T.expand); + collection.expandToggle.setAttribute('title', NAVIGATION_JS_T.expand); } siteTreeStatus.textContent = ''; @@ -240,6 +299,9 @@ function updateSearch() { display = updateVideo(video) || display; } + playlist.element.classList.remove('expanded'); + playlist.expandToggle.setAttribute('aria-label', NAVIGATION_JS_T.collapse); + playlist.expandToggle.setAttribute('title', NAVIGATION_JS_T.collapse); playlist.element.classList.toggle('visible', display); } @@ -259,6 +321,9 @@ function updateSearch() { updateCollection(subcollection); } + collection.element.classList.remove('expanded'); + collection.expandToggle.setAttribute('aria-label', NAVIGATION_JS_T.collapse); + collection.expandToggle.setAttribute('title', NAVIGATION_JS_T.collapse); collection.element.classList.toggle('visible', display); } @@ -351,7 +416,7 @@ navigation.addEventListener('keydown', event => { } else if (event.key === ' ') { if (event.target.classList.contains('collection') || event.target.classList.contains('playlist')) { - collapseExpand(event.target, undefined); + collapseExpand(event.target); event.preventDefault(); } else if (event.target.classList.contains('video')) { event.preventDefault(); @@ -380,15 +445,27 @@ navigation.addEventListener('keydown', event => { function collapseExpand(siteTreeElement, expand = undefined) { function visitPlaylist(playlist) { if (playlist.element === siteTreeElement) { + if (expand === undefined) { + expand = !playlist.element.classList.contains('expanded'); + } + for (const video of playlist.videos) { video.element.classList.toggle('visible', expand); } + playlist.element.classList.toggle('expanded', expand); + const expandOrCollapse = NAVIGATION_JS_T[expand ? 'collapse' : 'expand']; + playlist.expandToggle.setAttribute('aria-label', expandOrCollapse); + playlist.expandToggle.setAttribute('title', expandOrCollapse); + return true; } else if (!expand && playlist.videos.some(video => video.element === siteTreeElement)) { for (const video of playlist.videos) { video.element.classList.remove('visible'); } + playlist.element.classList.remove('expanded'); + playlist.expandToggle.setAttribute('aria-label', NAVIGATION_JS_T.expand); + playlist.expandToggle.setAttribute('title', NAVIGATION_JS_T.expand); playlist.element.focus(); return true; @@ -399,15 +476,27 @@ function collapseExpand(siteTreeElement, expand = undefined) { function visitCollection(collection) { if (collection.element === siteTreeElement) { + if (expand === undefined) { + expand = !collection.element.classList.contains('expanded'); + } + for (const video of collection.videos) { video.element.classList.toggle('visible', expand); } + collection.element.classList.toggle('expanded', expand); + const expandOrCollapse = NAVIGATION_JS_T[expand ? 'collapse' : 'expand']; + collection.expandToggle.setAttribute('aria-label', expandOrCollapse); + collection.expandToggle.setAttribute('title', expandOrCollapse); + return true; } else if (!expand && collection.videos.some(video => video.element === siteTreeElement)) { for (const video of collection.videos) { video.element.classList.remove('visible'); } + collection.element.classList.remove('expanded'); + collection.expandToggle.setAttribute('aria-label', NAVIGATION_JS_T.expand); + collection.expandToggle.setAttribute('title', NAVIGATION_JS_T.expand); collection.element.focus(); return true; diff --git a/src/build/assets/site.css b/src/build/assets/site.css index 8d5c258..2204a78 100644 --- a/src/build/assets/site.css +++ b/src/build/assets/site.css @@ -506,6 +506,10 @@ img { top: 3.5rem; z-index: 2; } +.site_tree [role="status"]:not(:empty) { + font-weight: 500; + padding: .5rem; +} .site_tree .elements { display: flex; flex-direction: column; @@ -514,9 +518,7 @@ img { .site_tree .elements a { align-items: center; border-radius: .2rem; - column-gap: .7rem; display: flex; - padding: .5rem; } .site_tree .elements a:focus-visible, .site_tree .elements a:hover { @@ -525,19 +527,58 @@ img { outline: none; } .site_tree .elements a:not(.visible) { display: none; } -.site_tree .elements .collection, -.site_tree .elements .playlist { - column-gap: .3rem; +.site_tree .elements .collection > :nth-child(1), +.site_tree .elements .playlist > :nth-child(1) { + align-items: center; + display: flex; + height: 1rem; + justify-content: center; + padding: .5rem; + width: 1rem; +} +.site_tree .elements .collection > :nth-child(1) svg, +.site_tree .elements .playlist > :nth-child(1) svg { + border-radius: 50%; + color: var(--fg-1); + font-size: .6rem; + padding: .2rem; +} +.site_tree .elements .collection > :nth-child(1):hover svg, +.site_tree .elements .playlist > :nth-child(1):hover svg { + background: var(--fg-3); + color: var(--bg-1); +} +.site_tree .elements .collection.expanded > :nth-child(1) svg, +.site_tree .elements .playlist.expanded > :nth-child(1) svg { + transform: rotate(90deg); } .site_tree .elements .collection > :nth-child(2), .site_tree .elements .playlist > :nth-child(2) { - color: var(--fg-3); + align-items: center; + column-gap: .3rem; + display: flex; } +.site_tree .elements .collection > :nth-child(2) svg, +.site_tree .elements .playlist > :nth-child(2) svg { + color: var(--mg); + font-size: .5rem; +} +.site_tree .elements .collection > :nth-child(3), +.site_tree .elements .playlist > :nth-child(3) { + color: var(--fg-3); + margin-left: .3rem; +} +.site_tree .elements .video { padding: .5rem; } .site_tree .elements .video > :nth-child(1) { aspect-ratio: var(--poster-aspect); + border-radius: .2rem; height: 3rem; + margin-left: 1.5rem; +} +.site_tree .elements .video > :nth-child(2) { + flex: 1; + margin-left: .7rem; } -.site_tree .elements .video > :nth-child(2) { flex: 1; } .site_tree .elements .placeholder { background: var(--bg-3); } .site_tree:not(.open) { display: none; } #speed .x { font-size: .8em; } diff --git a/src/build/layout.rs b/src/build/layout.rs index ec959c6..a7f11a6 100644 --- a/src/build/layout.rs +++ b/src/build/layout.rs @@ -102,6 +102,8 @@ impl Layout { String::new() }; + let mut templates = String::new(); + let browse; let search; let navigation_script; @@ -131,6 +133,15 @@ impl Layout { "#); navigation_script = format!(r#""#); + + let chevron_right_icon = icons::CHEVRON_RIGHT; + let navigation_templates = formatdoc!(r#" + + "#); + + templates.push_str(&navigation_templates); } else { browse = String::new(); search = String::new(); @@ -152,15 +163,6 @@ impl Layout { "#); - let breadcrumbs = traversal.parents(site) - .iter() - .map(|breadcrumb| { - let breadcrumb_rendered = breadcrumb.step.breadcrumb(&breadcrumb.href, site); - format!(r#"{breadcrumb_rendered} "#) - }) - .collect::>() - .join(""); - let dark_icon = icons::dark(translations.dark_color_scheme); let light_icon = icons::light(translations.light_color_scheme); let theme_toggle = match &site.theme.kind { @@ -175,8 +177,6 @@ impl Layout { let version_display = env!("HYPER8_VERSION_DISPLAY"); - let mut templates = String::new(); - if self.clipboard_script { templates.push_str(©_button_icon_templates(translations)); } diff --git a/src/build/outline.rs b/src/build/outline.rs index 9733583..9fc193d 100644 --- a/src/build/outline.rs +++ b/src/build/outline.rs @@ -731,8 +731,6 @@ impl TraversalStep<'_> { // String::from(r#"
"#) // }; - let label = playlist.title_or_slug_or_generic_label(translations); - if let Some(video) = video { let index_suffix = site.index_suffix(); diff --git a/src/build/scripts.rs b/src/build/scripts.rs index b34fc51..20e9044 100644 --- a/src/build/scripts.rs +++ b/src/build/scripts.rs @@ -142,11 +142,15 @@ pub fn generate_navigation_js( }; if let Some(site_content) = site_content { + let t_collapse = translations.collapse; + let t_expand = translations.expand; let t_nothing_found_for_xxx = js_escape_inside_single_quoted_string(translations.nothing_found_for_xxx); let t_showing_xxx_results_for_xxx = js_escape_inside_single_quoted_string(translations.showing_xxx_results_for_xxx); let t_xxx_videos = js_escape_inside_single_quoted_string(translations.xxx_videos); let mut js = formatdoc!(r#" const NAVIGATION_JS_T = {{ + collapse: '{t_collapse}', + expand: '{t_expand}', nothingFoundForXxx: query => '{t_nothing_found_for_xxx}'.replace('{{query}}', query), showingXxxResultsForXxx: (count, query) => '{t_showing_xxx_results_for_xxx}'.replace('{{count}}', count).replace('{{query}}', query), xxxVideos: count => '{t_xxx_videos}'.replace('{{count}}', count) diff --git a/src/icons.rs b/src/icons.rs index aad1607..a933319 100644 --- a/src/icons.rs +++ b/src/icons.rs @@ -20,6 +20,13 @@ pub const ATOM: &str = indoc!(r#" "#); +/// A rightwards-facing, perfectly perpendicular chevron (like a closing angled bracket with a 90 degrees angle) +pub const CHEVRON_RIGHT: &str = indoc!(r#" + +"#); + /// The stylized string "" pub const CODE: &str = indoc!(r#"