diff --git a/src/Elastic.Documentation.Site/Assets/web-components/Header/Header.tsx b/src/Elastic.Documentation.Site/Assets/web-components/Header/Header.tsx index e98f83d31f..b4a65df6b0 100644 --- a/src/Elastic.Documentation.Site/Assets/web-components/Header/Header.tsx +++ b/src/Elastic.Documentation.Site/Assets/web-components/Header/Header.tsx @@ -14,7 +14,7 @@ import { useRef } from 'react' interface Props { title: string - logoHref: string + logoHref?: string githubRepository?: string githubLink?: string gitBranch: string @@ -29,6 +29,8 @@ interface Props { * component does not have to infer branding state from other optional props. */ branded?: boolean + /** When true the git remote belongs to the `elastic` GitHub organization. Controls whether the Elastic logo is shown as default. */ + elasticOrg?: boolean /** Custom header background CSS colour. Only used when branded=true; defaults to #000000. */ headerBg?: string /** Custom icon image URL. When set (and branded=true), renders an instead of the title text. */ @@ -45,6 +47,7 @@ export const Header = ({ githubRef, airGapped = false, branded = false, + elasticOrg = false, headerBg, iconSrc, }: Props) => { @@ -52,51 +55,65 @@ export const Header = ({ const containerRef = useRef(null) useHtmxContainer(containerRef) - const logoSection = branded ? ( - iconSrc ? ( - // Branded with icon — plain , no HTMX (no containerRef) - + {title} - {title} - {title} + /> + {title} + + ) : ( + <>{title} + ) + + const brandedStyles = iconSrc + ? css` + display: inline-flex; + align-items: center; + gap: ${euiTheme.size.s}; + color: var(--color-white); + text-decoration: none; + padding: ${euiTheme.size.s}; + ` + : css` + display: inline-flex; + align-items: center; + color: var(--color-white); + text-decoration: none; + padding: ${euiTheme.size.s}; + font-weight: ${euiTheme.font.weight.bold}; + ` + + const plainStyles = css` + display: inline-flex; + align-items: center; + padding: ${euiTheme.size.s}; + font-weight: ${euiTheme.font.weight.bold}; + color: ${euiTheme.colors.textInk}; + text-decoration: none; + border-radius: ${euiTheme.border.radius.small}; + &:hover { + background: rgba(0, 0, 0, 0.06); + } + ` + + const logoSection = branded ? ( + logoHref ? ( + + {brandedIconContent} ) : ( - // Branded without icon — title text only, no HTMX - - {title} - + {brandedIconContent} ) - ) : ( - // Default: Elastic-branded logo (light-mode styling) + ) : elasticOrg ? ( + ) : ( + + {logoHref ? ( + + {title} + + ) : ( + {title} + )} + ) const headerCss = @@ -197,6 +224,7 @@ customElements.define( githubRef: 'string', airGapped: 'boolean', branded: 'boolean', + elasticOrg: 'boolean', headerBg: 'string', iconSrc: 'string', }, diff --git a/src/Elastic.Documentation.Site/Layout/_IsolatedHeader.cshtml b/src/Elastic.Documentation.Site/Layout/_IsolatedHeader.cshtml index c87249119b..3c7ea83183 100644 --- a/src/Elastic.Documentation.Site/Layout/_IsolatedHeader.cshtml +++ b/src/Elastic.Documentation.Site/Layout/_IsolatedHeader.cshtml @@ -8,6 +8,7 @@ git-commit="@(Model.GitCommitShort)" github-ref="@(Model.GitHubRef)" branded="@(Model.Branding != null ? "true" : "false")" + elastic-org="@(Model.IsElasticOrg ? "true" : "false")" header-bg="@(Model.Branding?.HeaderBg)" icon-src="@(Model.BrandingIconStaticPath)"> diff --git a/src/Elastic.Documentation.Site/_ViewModels.cs b/src/Elastic.Documentation.Site/_ViewModels.cs index 04593b02ec..2705824838 100644 --- a/src/Elastic.Documentation.Site/_ViewModels.cs +++ b/src/Elastic.Documentation.Site/_ViewModels.cs @@ -76,6 +76,10 @@ public record GlobalLayoutViewModel public bool RenderHamburgerIcon { get; init; } = true; + /// Whether the git remote belongs to the elastic GitHub organization. + public bool IsElasticOrg => + GitRepository?.StartsWith("elastic/", StringComparison.OrdinalIgnoreCase) == true; + /// White-label branding overrides. When non-null, all Elastic-specific chrome is suppressed. public BrandingConfiguration? Branding { get; init; } diff --git a/src/Elastic.Markdown/DocumentationGenerator.cs b/src/Elastic.Markdown/DocumentationGenerator.cs index c0179760f2..cdca65b33a 100644 --- a/src/Elastic.Markdown/DocumentationGenerator.cs +++ b/src/Elastic.Markdown/DocumentationGenerator.cs @@ -232,11 +232,22 @@ private void CopyBrandingResources() } var destination = _writeFileSystem.FileInfo.New(Path.Join(outputStaticDir, source.Name)); - _ = source.CopyTo(destination.FullName, overwrite: true); + CopyFileAcrossFileSystems(source, destination); _logger.LogInformation("Copied branding asset {Source} -> {Destination}", source.FullName, destination.FullName); } } + private void CopyFileAcrossFileSystems(IFileInfo source, IFileInfo destination) + { + if (Context.ReadFileSystem == _writeFileSystem) + Context.ReadFileSystem.File.Copy(source.FullName, destination.FullName, overwrite: true); + else + { + var bytes = Context.ReadFileSystem.File.ReadAllBytes(source.FullName); + _writeFileSystem.File.WriteAllBytes(destination.FullName, bytes); + } + } + private void HintUnusedSubstitutionKeys() { var definedKeys = new HashSet(Context.Configuration.Substitutions.Keys.ToArray()); diff --git a/tests/Elastic.Markdown.Tests/BrandingCopyTests.cs b/tests/Elastic.Markdown.Tests/BrandingCopyTests.cs new file mode 100644 index 0000000000..22ff9a961d --- /dev/null +++ b/tests/Elastic.Markdown.Tests/BrandingCopyTests.cs @@ -0,0 +1,60 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.IO.Abstractions.TestingHelpers; +using AwesomeAssertions; +using Elastic.Documentation; +using Elastic.Documentation.Configuration; +using Elastic.Documentation.Diagnostics; +using Elastic.Markdown.IO; + +namespace Elastic.Markdown.Tests; + +public class BrandingCopyTests(ITestOutputHelper output) +{ + [Fact] + public async Task CopyBrandingResources_SeparateFileSystems_DoesNotThrow() + { + var logger = new TestLoggerFactory(output); + + var readFs = new MockFileSystem(new Dictionary + { + { "docs/docset.yml", + //language=yaml + new MockFileData(""" +project: test +toc: +- file: index.md +branding: + icon: assets/logo.svg +""") }, + { "docs/index.md", new MockFileData("# Hello") }, + { "docs/assets/logo.svg", new MockFileData("") } + }, new MockFileSystemOptions + { + CurrentDirectory = Paths.WorkingDirectoryRoot.FullName + }); + + var writeFs = new MockFileSystem(new MockFileSystemOptions + { + CurrentDirectory = Paths.WorkingDirectoryRoot.FullName + }); + + await using var collector = new DiagnosticsCollector([]).StartAsync(TestContext.Current.CancellationToken); + var configurationContext = TestHelpers.CreateConfigurationContext(readFs); + var readScoped = FileSystemFactory.ScopeCurrentWorkingDirectory(readFs); + var writeScoped = FileSystemFactory.ScopeCurrentWorkingDirectoryForWrite(writeFs); + var context = new BuildContext(collector, readScoped, writeScoped, configurationContext, ExportOptions.Default); + + var linkResolver = new TestCrossLinkResolver(); + var set = new DocumentationSet(context, logger, linkResolver); + var generator = new DocumentationGenerator(set, logger); + + await generator.GenerateAll(TestContext.Current.CancellationToken); + await collector.StopAsync(TestContext.Current.CancellationToken); + + var outputStaticDir = Path.Join(set.OutputDirectory.FullName, "_static"); + writeFs.File.Exists(Path.Join(outputStaticDir, "logo.svg")).Should().BeTrue(); + } +}