diff --git a/config/navigation_preview.yml b/config/navigation_preview.yml index 2357922a6..a1d7d891d 100644 --- a/config/navigation_preview.yml +++ b/config/navigation_preview.yml @@ -46,6 +46,20 @@ toc: - section: APIs external: https://www.elastic.co/docs/api/ + ########### + # PRODUCTS # + ########### + - section: Products + dropdown: + - title: Elasticsearch + url: solutions/search/elasticsearch + - title: Observability + url: solutions/observability + - title: Security + url: solutions/security + - title: Elastic Cloud + url: deploy-manage/deploy/elastic-cloud + ############# # REFERENCE # ############# diff --git a/src/Elastic.Documentation.Configuration/Toc/SiteNavigationFile.cs b/src/Elastic.Documentation.Configuration/Toc/SiteNavigationFile.cs index 00cca58d8..d942e2f25 100644 --- a/src/Elastic.Documentation.Configuration/Toc/SiteNavigationFile.cs +++ b/src/Elastic.Documentation.Configuration/Toc/SiteNavigationFile.cs @@ -23,13 +23,19 @@ public interface ISiteNavigationEntry IReadOnlyCollection Children { get; } } +/// A link entry within a dropdown: section. +public record SiteDropdownLinkRef(string Title, string Url); + public record SiteSectionRef( string Title, string? ExternalUrl, - IReadOnlyCollection Children + IReadOnlyCollection Children, + IReadOnlyCollection DropdownLinks ) : ISiteNavigationEntry { public bool IsExternal => ExternalUrl is not null; + /// True when the section carries a dropdown list instead of tree children. + public bool IsDropdown => DropdownLinks.Count > 0; } [YamlSerializable] @@ -207,6 +213,35 @@ public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeseria } value = childrenList; } + else if (key.Value is "dropdown") + { + var dropdownList = new List(); + _ = parser.Consume(); + while (!parser.TryConsume(out _)) + { + if (!parser.TryConsume(out _)) + continue; + string? itemTitle = null; + string? itemUrl = null; + while (!parser.TryConsume(out _)) + { + var itemKey = parser.Consume(); + if (parser.Accept(out var itemValue)) + { + _ = parser.MoveNext(); + if (itemKey.Value is "title") + itemTitle = itemValue.Value; + else if (itemKey.Value is "url") + itemUrl = itemValue.Value; + } + else + parser.SkipThisAndNestedEvents(); + } + if (itemTitle is not null && itemUrl is not null) + dropdownList.Add(new SiteDropdownLinkRef(itemTitle, itemUrl)); + } + value = dropdownList; + } else parser.SkipThisAndNestedEvents(); } @@ -222,7 +257,10 @@ public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeseria IReadOnlyCollection children = dictionary.TryGetValue("children", out var childrenObj) && childrenObj is List refs ? refs : []; - return new SiteSectionRef(sectionTitle, externalUrl, children); + IReadOnlyCollection dropdownLinks = dictionary.TryGetValue("dropdown", out var dropdownObj) && dropdownObj is List dLinks + ? dLinks + : []; + return new SiteSectionRef(sectionTitle, externalUrl, children, dropdownLinks); } if (dictionary.TryGetValue("toc", out var tocPath) && tocPath is string sourceString) diff --git a/src/Elastic.Documentation.Navigation/Assembler/SectionTopNavBuilder.cs b/src/Elastic.Documentation.Navigation/Assembler/SectionTopNavBuilder.cs index 71d7a3590..c48a3b08d 100644 --- a/src/Elastic.Documentation.Navigation/Assembler/SectionTopNavBuilder.cs +++ b/src/Elastic.Documentation.Navigation/Assembler/SectionTopNavBuilder.cs @@ -9,12 +9,14 @@ namespace Elastic.Documentation.Navigation.Assembler; /// /// Builds a from the top-level navigation entries in /// navigation_preview.yml when the navigation-preview feature flag is on. -/// Supports two entry shapes: +/// Supports three section: shapes: /// -/// toc: — a single navigation root, becomes one tab. -/// section: — a named group of toc: refs, becomes one tab whose active state -/// matches the section's navigation root. External sections become external-link tabs. +/// external: — external-link tab, never active. +/// dropdown: — a panel of links, never active (no tree membership). +/// children: — maps to a tree node; active when the +/// current page's NavigationRoot.Id equals the section's Id. /// +/// Plain toc: entries also produce one tab, active when NavigationRoot.Id == item.Id. /// Active state is determined by comparing the current page's NavigationRoot.Id to each /// tab's stored . /// @@ -48,6 +50,15 @@ public static class SectionTopNavBuilder { items.Add(new TopNavLinkItem(section.Title, section.ExternalUrl!, IsExternal: true)); } + else if (section.IsDropdown) + { + // Resolve each link URL against the site prefix so hrefs in the template are site-absolute. + var sitePrefix = navigation.Url.TrimEnd('/'); + var links = section.DropdownLinks + .Select(l => new TopNavLinkItem(l.Title, sitePrefix + "/" + l.Url.TrimStart('/'), IsExternal: false)) + .ToArray(); + items.Add(new TopNavDropdownItem(section.Title, [new TopNavGroup(null, links)])); + } else if (sectionsByTitle.TryGetValue(section.Title, out var sectionNav)) { // All pages within the section have NavigationRoot = sectionNav, diff --git a/src/Elastic.Documentation.Site/Assets/secondary-nav.css b/src/Elastic.Documentation.Site/Assets/secondary-nav.css index 9552f9239..2ee0c487d 100644 --- a/src/Elastic.Documentation.Site/Assets/secondary-nav.css +++ b/src/Elastic.Documentation.Site/Assets/secondary-nav.css @@ -1,9 +1,11 @@ @layer components { - .secondary-nav-mobile-menu summary { + .secondary-nav-mobile-menu summary, + .secondary-nav-mobile-submenu summary { list-style: none; } - .secondary-nav-mobile-menu summary::-webkit-details-marker { + .secondary-nav-mobile-menu summary::-webkit-details-marker, + .secondary-nav-mobile-submenu summary::-webkit-details-marker { display: none; } @@ -13,7 +15,10 @@ transition: transform 0.15s ease; } - .secondary-nav-mobile-menu[open] .secondary-nav-mobile-chevron { + .secondary-nav-mobile-menu[open] > summary .secondary-nav-mobile-chevron, + .secondary-nav-mobile-submenu[open] + > summary + .secondary-nav-mobile-chevron { transform: rotate(180deg); } } diff --git a/src/Elastic.Documentation.Site/Layout/_PagesNav.cshtml b/src/Elastic.Documentation.Site/Layout/_PagesNav.cshtml index 7420b55dc..5d3a94a2f 100644 --- a/src/Elastic.Documentation.Site/Layout/_PagesNav.cshtml +++ b/src/Elastic.Documentation.Site/Layout/_PagesNav.cshtml @@ -2,11 +2,12 @@ @using Microsoft.AspNetCore.Html @inherits RazorSlice @{ - var topNavLinks = Model.TopNav?.Items.OfType().ToArray() ?? []; + var topNavItems = Model.TopNav?.Items.ToArray() ?? []; + var topNavLinks = topNavItems.OfType().ToArray(); var currentSectionId = Model.CurrentNavigationItem.NavigationRoot?.Id; var activeMobileLink = topNavLinks.FirstOrDefault(item => item.IsActive(currentSectionId)); var mobileSummary = activeMobileLink?.Title ?? "Docs Home"; - var hasMobileDrawerControls = topNavLinks.Length > 0 || Model.ShowVersionDropdown; + var hasMobileDrawerControls = topNavItems.Length > 0 || Model.ShowVersionDropdown; }