Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { useRef } from 'react'

interface Props {
title: string
logoHref: string
logoHref?: string
githubRepository?: string
githubLink?: string
gitBranch: string
Expand All @@ -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 <img> instead of the title text. */
Expand All @@ -45,58 +47,73 @@ export const Header = ({
githubRef,
airGapped = false,
branded = false,
elasticOrg = false,
headerBg,
iconSrc,
}: Props) => {
const { euiTheme } = useEuiTheme()
const containerRef = useRef<HTMLSpanElement>(null)
useHtmxContainer(containerRef)

const logoSection = branded ? (
iconSrc ? (
// Branded with icon — plain <a>, no HTMX (no containerRef)
<a
href={logoHref}
const brandedIconContent = iconSrc ? (
<>
<img
src={iconSrc}
alt={title}
css={css`
display: inline-flex;
align-items: center;
gap: ${euiTheme.size.s};
color: var(--color-white);
text-decoration: none;
padding: ${euiTheme.size.s};
height: 32px;
width: auto;
`}
>
<img
src={iconSrc}
alt={title}
css={css`
height: 32px;
width: auto;
`}
/>
{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 ? (
<a href={logoHref} css={brandedStyles}>
{brandedIconContent}
</a>
) : (
// Branded without icon — title text only, no HTMX
<a
href={logoHref}
css={css`
display: inline-flex;
align-items: center;
color: var(--color-white);
text-decoration: none;
padding: ${euiTheme.size.s};
font-weight: ${euiTheme.font.weight.bold};
`}
>
{title}
</a>
<span css={brandedStyles}>{brandedIconContent}</span>
)
) : (
// Default: Elastic-branded logo (light-mode styling)
) : elasticOrg ? (
<span ref={containerRef}>
<EuiHeaderLogo
href={logoHref}
href={logoHref ?? undefined}
css={css`
padding-block: 7px;
height: auto;
Expand All @@ -113,6 +130,16 @@ export const Header = ({
{title}
</EuiHeaderLogo>
</span>
) : (
<span ref={containerRef}>
{logoHref ? (
<a href={logoHref} css={plainStyles}>
{title}
</a>
) : (
<span css={plainStyles}>{title}</span>
)}
</span>
)

const headerCss =
Expand Down Expand Up @@ -197,6 +224,7 @@ customElements.define(
githubRef: 'string',
airGapped: 'boolean',
branded: 'boolean',
elasticOrg: 'boolean',
headerBg: 'string',
iconSrc: 'string',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)">
</elastic-docs-header>
Expand Down
4 changes: 4 additions & 0 deletions src/Elastic.Documentation.Site/_ViewModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ public record GlobalLayoutViewModel

public bool RenderHamburgerIcon { get; init; } = true;

/// <summary>Whether the git remote belongs to the <c>elastic</c> GitHub organization.</summary>
public bool IsElasticOrg =>
GitRepository?.StartsWith("elastic/", StringComparison.OrdinalIgnoreCase) == true;

/// <summary>White-label branding overrides. When non-null, all Elastic-specific chrome is suppressed.</summary>
public BrandingConfiguration? Branding { get; init; }

Expand Down
13 changes: 12 additions & 1 deletion src/Elastic.Markdown/DocumentationGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>(Context.Configuration.Substitutions.Keys.ToArray());
Expand Down
60 changes: 60 additions & 0 deletions tests/Elastic.Markdown.Tests/BrandingCopyTests.cs
Original file line number Diff line number Diff line change
@@ -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<string, MockFileData>
{
{ "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("<svg/>") }
}, 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();
}
}
Loading