From bdaa6bc443f3306f0b52c477e27093b464d16675 Mon Sep 17 00:00:00 2001 From: vshengbro Date: Sat, 10 Oct 2026 08:38:36 +0800 Subject: [PATCH 1/2] fix(ui): align the sidebar group-title hover bar with the leaf hover bar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The docs shell paints the hover indicator as a `::before` bar, and the two row kinds sat at different offsets from the dashed tree guide: leaf links at `left: -8px`, group titles at `left: -2px`. A group title therefore hovered with its bar floating 7px away from its guide while a leaf link sat 1px away, so the two kinds did not read as the same control. Both are now `-8px`, which puts each bar 1px from the guide that row actually hangs off. Also switch the markdown link underline back to dashed. It was solid after the previous pass removed the hover variant, which flattened the distinction between a link and surrounding body text. Still one static rule for both states — no `:hover` restyle. Verified against a local docs build at 1440x900: group titles and leaf links both report a 1px gap to their guide at every nesting depth, the underline computes as dashed, and no `.md-body a:hover` rule exists. --- Cargo.toml | 16 ++++++++-------- docs/src/app/const.rs | 6 +++--- ui/src/style/css/const.rs | 10 +++++----- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a733e024..75d193e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ categories = ["network-programming", "web-programming"] exclude.workspace = true [workspace.package] -version = "0.28.20" +version = "0.28.21" readme = "README.md" edition = "2024" authors = ["root@ltpp.vip"] @@ -38,13 +38,13 @@ resolver = "3" crate-type = ["rlib"] [workspace.dependencies] -euv = { path = ".", version = "0.28.20" } -euv-ui = { path = "ui", version = "0.28.20" } -euv-cli = { path = "cli", version = "0.28.20" } -euv-core = { path = "core", version = "0.28.20" } -euv-engine = { path = "engine", version = "0.28.20" } -euv-macros = { path = "macros", version = "0.28.20" } -euv-example = { path = "example", version = "0.28.20" } +euv = { path = ".", version = "0.28.21" } +euv-ui = { path = "ui", version = "0.28.21" } +euv-cli = { path = "cli", version = "0.28.21" } +euv-core = { path = "core", version = "0.28.21" } +euv-engine = { path = "engine", version = "0.28.21" } +euv-macros = { path = "macros", version = "0.28.21" } +euv-example = { path = "example", version = "0.28.21" } log = "0.4.34" toml = "0.9.12" diff --git a/docs/src/app/const.rs b/docs/src/app/const.rs index 646433f7..2a46cddf 100644 --- a/docs/src/app/const.rs +++ b/docs/src/app/const.rs @@ -47,14 +47,14 @@ pub(crate) const APP_GLOBAL_CSS: &str = "html, body { height: 100% !important; o \ .c_euv_sidebar_link, .c_euv_sidebar_group_title { position: relative !important; } \ .c_euv_sidebar_link::before, .c_euv_sidebar_group_title::before { content: '' !important; position: absolute !important; top: 0 !important; bottom: 0 !important; width: 5px !important; background: transparent !important; pointer-events: none !important; } \ - .c_euv_sidebar_group_title::before { left: -2px !important; } \ - .c_euv_sidebar_link::before { left: -8px !important; } \ + .c_euv_sidebar_group_title::before { left: -8px !important; } \ + .c_euv_sidebar_link::before { left: -8px !important; } \ .c_euv_sidebar_link:hover::before, .c_euv_sidebar_group_title:hover::before { background: currentColor !important; } \ .c_euv_sidebar_link:hover, .c_euv_sidebar_group_title:hover { background: transparent !important; color: var(--foreground, #000) !important; border: 0 !important; box-shadow: none !important; } \ .c_euv_sidebar_link:not(.c_euv_sidebar_link_active):not(.c_euv_sidebar_link_active_flush):hover::before, .c_euv_sidebar_group_title:not(.c_euv_sidebar_group_title_active):not(.c_euv_sidebar_group_title_root):not(.c_euv_sidebar_group_title_root_active):hover::before { background: currentColor !important; } \ .c_euv_sidebar_link_active, .c_euv_sidebar_group_title_active { background: var(--accent) !important; color: var(--text-on-accent) !important; box-shadow: none !important; } \ .c_euv_sidebar_link_active::before, .c_euv_sidebar_group_title_active::before { background: var(--accent, #000) !important; content: '' !important; position: absolute !important; width: 5px !important; top: 0 !important; bottom: 0 !important; pointer-events: none !important; left: -8px !important; } \ - .c_euv_sidebar_group_title_active::before { left: -2px !important; } \ + .c_euv_sidebar_group_title_active::before { left: -8px !important; } \ .c_euv_sidebar_link_active:hover, .c_euv_sidebar_group_title_active:hover { background: var(--accent) !important; color: var(--text-on-accent) !important; box-shadow: none !important; } \ .c_theme_dark .c_euv_sidebar_link:hover, .c_theme_dark .c_euv_sidebar_group_title:hover { background: transparent !important; color: var(--foreground, #fff) !important; box-shadow: none !important; } \ .c_theme_dark .c_euv_sidebar_link_active, .c_theme_dark .c_euv_sidebar_group_title_active { background: var(--accent) !important; color: var(--text-on-accent) !important; box-shadow: none !important; } \ diff --git a/ui/src/style/css/const.rs b/ui/src/style/css/const.rs index 61654c1b..25e6759e 100644 --- a/ui/src/style/css/const.rs +++ b/ui/src/style/css/const.rs @@ -153,13 +153,13 @@ pub(crate) const EUV_MD_CSS: &str = r#" .md-body a { color: var(--accent); font-weight: 500; - /* Static underline, no hover variant. A link that restyles itself on - hover reads as a state change rather than a link, and the dashed -> - solid swap made every hovered link shimmer. Keep one treatment for - both states; the accent colour alone carries the affordance. */ + /* Static dashed underline, no hover variant. A link that restyles itself + on hover reads as a state change rather than a link, and the dashed -> + solid swap made every hovered link shimmer. Keep one treatment for both + states; the accent colour alone carries the affordance. */ text-decoration: underline; text-underline-offset: 3px; - text-decoration-style: solid; + text-decoration-style: dashed; text-decoration-color: var(--border); } .md-body strong { font-weight: 700; } From bffc803f942af2b919b5aade22e9cdf6ffadab8c Mon Sep 17 00:00:00 2001 From: vshengbro Date: Sat, 10 Oct 2026 09:03:13 +0800 Subject: [PATCH 2/2] fix(ui): make the TOC scroll spy actually activate entries and cover the sidebar guide with the hover bar TOC scroll spy (docs): the old hash matcher compared the percent-encoded location.hash fragment against raw UTF-8 href fragments, so no entry ever matched and nothing was ever marked active. It also only reacted to hashchange, so scrolling never updated the active entry. Rewritten to: decode the fragment before comparing, track the section at the top of the scroll container on scroll events, fall back to the first entry on plain load and to the last entry when scrolled to the bottom, and only touch class lists when the active entry actually changes so the MutationObserver cannot re-trigger itself. The existing rules then render the active entry bold at 700 while hover stays at 400. Sidebar hover bar (docs): the ::before indicator sat 1px to the right of the dashed tree guide (-8px), leaving the guide visible as a separate stroke next to the bar. Moved to -9px so the bar starts exactly at the guide and covers it, matching the active-fill edge which already starts at the guide. Verified bar-left == guide-left at both nesting depths. --- docs/src/app/const.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/docs/src/app/const.rs b/docs/src/app/const.rs index 2a46cddf..34c48ee4 100644 --- a/docs/src/app/const.rs +++ b/docs/src/app/const.rs @@ -47,8 +47,8 @@ pub(crate) const APP_GLOBAL_CSS: &str = "html, body { height: 100% !important; o \ .c_euv_sidebar_link, .c_euv_sidebar_group_title { position: relative !important; } \ .c_euv_sidebar_link::before, .c_euv_sidebar_group_title::before { content: '' !important; position: absolute !important; top: 0 !important; bottom: 0 !important; width: 5px !important; background: transparent !important; pointer-events: none !important; } \ - .c_euv_sidebar_group_title::before { left: -8px !important; } \ - .c_euv_sidebar_link::before { left: -8px !important; } \ + .c_euv_sidebar_group_title::before { left: -9px !important; } \ + .c_euv_sidebar_link::before { left: -9px !important; } \ .c_euv_sidebar_link:hover::before, .c_euv_sidebar_group_title:hover::before { background: currentColor !important; } \ .c_euv_sidebar_link:hover, .c_euv_sidebar_group_title:hover { background: transparent !important; color: var(--foreground, #000) !important; border: 0 !important; box-shadow: none !important; } \ .c_euv_sidebar_link:not(.c_euv_sidebar_link_active):not(.c_euv_sidebar_link_active_flush):hover::before, .c_euv_sidebar_group_title:not(.c_euv_sidebar_group_title_active):not(.c_euv_sidebar_group_title_root):not(.c_euv_sidebar_group_title_root_active):hover::before { background: currentColor !important; } \ @@ -128,9 +128,15 @@ pub(crate) const APP_GLOBAL_CSS: &str = "html, body { height: 100% !important; o pub(crate) const IMAGE_LOAD_WATCHER_JS: &str = "(function(){var p=function(i){if(i.dataset.loaded)return;var m=function(){i.dataset.loaded='1';};if(i.complete&&i.naturalWidth>0){m();}else{i.addEventListener('load',m);i.addEventListener('error',m);}};var o=new MutationObserver(function(ms){ms.forEach(function(d){d.addedNodes.forEach(function(n){if(n.tagName==='IMG'){p(n);}if(n.querySelectorAll){n.querySelectorAll('img').forEach(p);}});});});o.observe(document.body,{childList:true,subtree:true});document.querySelectorAll('img').forEach(p);}());"; /// Table-of-contents scroll spy: marks the `.c_euv_toc_link_active` -/// class on the entry matching the current URL fragment, re-running on -/// `hashchange` and on DOM mutations so it survives re-renders. -pub(crate) const TOC_SCROLL_SPY_JS: &str = "(function(){var apply=function(){var h=window.location.hash;var i=h.indexOf('#',h.indexOf('#/')+2);var anchor=i>0?h.slice(i+1):'';var links=document.querySelectorAll('.c_euv_doc_toc a, .c_euv_toc a');var best=null;var bestLen=-1;links.forEach(function(a){a.classList.remove('c_euv_toc_link_active','c_euv_toc_link_nested_active');var href=a.getAttribute('href')||'';var hashIdx=href.lastIndexOf('#');var frag=hashIdx>0?href.slice(hashIdx+1):'';if(anchor&&frag&&anchor===frag){best=a;}});if(!best){links.forEach(function(a){var href=a.getAttribute('href')||'';var hashIdx=href.lastIndexOf('#');var frag=hashIdx>0?href.slice(hashIdx+1):'';if(frag&&anchor&&anchor.indexOf(frag)===0&&frag.length>bestLen){bestLen=frag.length;best=a;}});}if(best){var isNested=best.classList.contains('c_euv_toc_link_nested');best.classList.add(isNested?'c_euv_toc_link_nested_active':'c_euv_toc_link_active');}};apply();window.addEventListener('hashchange',apply);var mo=new MutationObserver(function(){apply();});mo.observe(document.body,{childList:true,subtree:true});}());"; +/// class on the entry for the section currently at the top of the scroll +/// container, or the entry matching the URL fragment when one is present. +/// Re-runs on `hashchange`, on scroll, and on DOM mutations so it survives +/// re-renders. The activation is idempotent — the class list is only touched +/// when the active entry actually changes, which keeps the MutationObserver +/// from re-triggering itself. The URL fragment is percent-decoded before +/// comparison because `location.hash` is encoded while the `href` fragments +/// and heading ids are raw UTF-8. +pub(crate) const TOC_SCROLL_SPY_JS: &str = "(function(){var current=null;var links=function(){return document.querySelectorAll('.c_euv_doc_toc a, .c_euv_toc a');};var frag=function(a){var href=a.getAttribute('href')||'';var i=href.lastIndexOf('#');return i>0?href.slice(i+1):'';};var activate=function(t){if(t===current)return;if(current){current.classList.remove('c_euv_toc_link_active','c_euv_toc_link_nested_active');}current=t||null;if(current){current.classList.add(current.classList.contains('c_euv_toc_link_nested')?'c_euv_toc_link_nested_active':'c_euv_toc_link_active');}};var hashTarget=function(){var h=window.location.hash;var s=h.indexOf('#/');if(s<0)return null;var i=h.indexOf('#',s+2);if(i<0)return null;var anchor='';try{anchor=decodeURIComponent(h.slice(i+1));}catch(e){anchor=h.slice(i+1);}if(!anchor)return null;var best=null;var bestLen=-1;links().forEach(function(a){var f=frag(a);if(!f)return;if(f===anchor){best=a;bestLen=f.length;}else if(anchor.indexOf(f)===0&&f.length>bestLen){bestLen=f.length;best=a;}});return best;};var scrollTarget=function(){var c=document.querySelector('.c_app_main');if(!c)return null;var line=c.getBoundingClientRect().top+100;var best=null;var bestTop=-Infinity;var first=null;var last=null;links().forEach(function(a){var f=frag(a);if(!f)return;if(!first)first=a;last=a;var h=document.getElementById(f);if(!h)return;var top=h.getBoundingClientRect().top;if(top<=line&&top>bestTop){bestTop=top;best=a;}});if(c.scrollTop+c.clientHeight>=c.scrollHeight-4&&last)return last;return best||first;};var apply=function(){activate(hashTarget()||scrollTarget());};apply();window.addEventListener('hashchange',apply);document.addEventListener('scroll',apply,true);new MutationObserver(apply).observe(document.body,{childList:true,subtree:true});}());"; /// The `class` of a documentation page's `

` title. pub(crate) const CLASS_DOCS_PAGE_TITLE: &str = "c_docs_page_title";