Skip to content
Draft
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
15 changes: 15 additions & 0 deletions docs/syntax/get-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,21 @@ A step takes one of three shapes.
url-label: How to publish
```

When an option offers variants of one command, use `commands` instead of `code`. Each command takes a short `label` and gets its own code block, so each copy button copies exactly one command. All commands use the option's `language`.

```yaml
- label: Run locally
description: Spin up Elasticsearch on your machine for development with `start-local`.
commands:
- label: Elasticsearch only
code: curl -fsSL https://elastic.co/start-local | sh -s -- --esonly
- label: Elasticsearch and Kibana
code: curl -fsSL https://elastic.co/start-local | sh
language: sh
```

An option takes either `code` or `commands`, not both.

Steps are numbered automatically, in source order. Each number sits to the left of its title.

## Links
Expand Down
38 changes: 33 additions & 5 deletions src/Elastic.Documentation.Site/Assets/markdown/hub.css
Original file line number Diff line number Diff line change
Expand Up @@ -228,20 +228,21 @@
--code-size: var(--text-xs);
--code-leading: 1.2;
}
/* A long command scrolls sideways inside its box instead of being clipped. */
/* A long command wraps inside its box, so the whole command stays visible.
The box keeps the option button's height for a one-line command. */
.hub-get-started .hub-get-started-install pre {
display: flex;
align-items: center;
box-sizing: border-box;
height: 37px;
min-height: 37px;
margin: 0;
padding: 0 36px 0 12px;
overflow-x: auto;
padding: 8px 36px 8px 12px;
background: var(--color-ink-navy);
border-radius: 8px;
}
.hub-get-started .hub-get-started-install code {
white-space: nowrap;
white-space: pre-wrap;
overflow-wrap: anywhere;
}

.hub-get-started .hub-get-started-track {
Expand Down Expand Up @@ -383,9 +384,36 @@
line-height: 1.45;
}
.hub-get-started .hub-get-started-option .hub-get-started-install,
.hub-get-started .hub-get-started-option .hub-get-started-commands,
.hub-get-started .hub-get-started-option-btn {
margin-top: auto;
}
/* Labeled variants of one command, stacked, each in its own copyable chip. */
.hub-get-started .hub-get-started-commands {
display: flex;
flex-direction: column;
gap: 6px;
}
/* Stacked chips share one width, so their edges line up. */
.hub-get-started .hub-get-started-commands .hub-get-started-install {
width: 100%;
margin-top: 0;
}
.hub-get-started .hub-get-started-command-label {
font-size: var(--text-sm);
font-weight: 600;
color: var(--color-ink-light, #4c4c4c);
}
.hub-get-started .hub-get-started-command-label:not(:first-child) {
margin-top: 8px;
}
/* A command stack is taller than a button, so a button beside one stays under its own
text instead of dropping to the bottom of the row. */
.hub-get-started
.hub-get-started-fork:has(.hub-get-started-commands)
.hub-get-started-option-btn {
margin-top: 0;
}
/* Same height as the command chip, and smaller than the body type. */
.hub-get-started .hub-get-started-option-btn {
@apply inline-flex items-center justify-center;
Expand Down
22 changes: 22 additions & 0 deletions src/Elastic.Documentation.Site/Components/Pages/Hub/hub.css
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,28 @@ a:focus-visible .hub-arrow {
color: var(--color-code-text);
}

/* Labeled variants of one command, stacked, each with its own copy button. */
.get-started__commands {
display: flex;
flex-direction: column;
gap: var(--space-2);
margin-top: auto;
}

.get-started__commands .get-started__code {
margin-top: 0;
}

.get-started__command-label {
color: var(--color-text-muted);
font-size: var(--text-xs);
font-weight: var(--weight-semibold);
}

.get-started__command-label:not(:first-child) {
margin-top: var(--space-2);
}

.get-started__prompt {
color: var(--color-code-keyword);
font-family: var(--font-mono);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,13 @@ .. step.Options.Select(
Label = option.Label,
DescriptionHtml = RenderInlineMarkdown(option.Description),
Code = option.Code,
Commands =
[
.. option
.Commands
.Where(command => !string.IsNullOrWhiteSpace(command.Code))
.Select(command => new GetStartedCommandViewModel { Label = command.Label, Code = command.Code! })
],
Language = option.Language,
Url = option.Url,
UrlLabel = option.UrlLabel
Expand Down
15 changes: 15 additions & 0 deletions src/Elastic.Markdown/Myst/Directives/Hub/GetStartedBlock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ public override void FinalizeAndValidate(ParserContext context)
step.Description = DirectiveLinkValidator.ResolveMarkdownLinks(step.Description, this, context);
foreach (var option in step.Options)
{
if (!string.IsNullOrWhiteSpace(option.Code) && option.Commands.Length > 0)
this.EmitError("{get-started} option takes either `code` or `commands`, not both.");
option.Url = DirectiveLinkValidator.ValidateAndResolve(option.Url, this, context);
option.Description = DirectiveLinkValidator.ResolveMarkdownLinks(option.Description, this, context);
}
Expand Down Expand Up @@ -112,6 +114,10 @@ public record GetStartedStepOption
[YamlPropertyName("code")]
public string? Code { get; set; }

/// <summary>Labeled variants of one command. Each gets its own code block and copy button.</summary>
[YamlPropertyName("commands")]
public GetStartedCommand[] Commands { get; set; } = [];

[YamlPropertyName("language")]
public string? Language { get; set; }

Expand All @@ -121,3 +127,12 @@ public record GetStartedStepOption
[YamlPropertyName("url-label")]
public string? UrlLabel { get; set; }
}

public record GetStartedCommand
{
[YamlPropertyName("label")]
public string? Label { get; set; }

[YamlPropertyName("code")]
public string? Code { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,30 @@
{
<p class="get-started__option-desc">@(new HtmlString(option.DescriptionHtml))</p>
}
@if (!string.IsNullOrWhiteSpace(option.Code))
@if (option.HasCode)
{
<div class="get-started__code">
<span class="get-started__prompt" aria-hidden="true">$</span>
<pre><code class="language-@(string.IsNullOrWhiteSpace(option.Language) ? "text" : option.Language)">@option.Code</code></pre>
<button class="get-started__copy" type="button" data-copy-text="@option.Code" hidden>Copy</button>
var language = string.IsNullOrWhiteSpace(option.Language) ? "text" : option.Language;
IReadOnlyList<Elastic.Markdown.Myst.Directives.Hub.GetStartedCommandViewModel> commands = string.IsNullOrWhiteSpace(option.Code)
? option.Commands
: [new() { Label = null, Code = option.Code }];
<div class="get-started__commands">
@foreach (var command in commands)
{
@if (!string.IsNullOrWhiteSpace(command.Label))
{
<span class="get-started__command-label">@command.Label</span>
}
<div class="get-started__code">
<span class="get-started__prompt" aria-hidden="true">$</span>
<pre><code class="language-@language">@command.Code</code></pre>
<button class="get-started__copy" type="button" data-copy-text="@command.Code" hidden>Copy</button>
</div>
}
</div>
}
@if (!string.IsNullOrWhiteSpace(option.Url))
{
<a class="@(string.IsNullOrWhiteSpace(option.Code) ? "btn btn--primary btn--sm" : "get-started__link")" @Model.LinkAttributes(option.Url)>@(string.IsNullOrWhiteSpace(option.UrlLabel) ? (string.IsNullOrWhiteSpace(option.Code) ? "Get started" : "Learn more") : option.UrlLabel)</a>
<a class="@(option.HasCode ? "get-started__link" : "btn btn--primary btn--sm")" @Model.LinkAttributes(option.Url)>@(string.IsNullOrWhiteSpace(option.UrlLabel) ? (option.HasCode ? "Learn more" : "Get started") : option.UrlLabel)</a>
}
</div>
}
Expand Down
27 changes: 20 additions & 7 deletions src/Elastic.Markdown/Myst/Directives/Hub/GetStartedView.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -135,17 +135,21 @@
if (!string.IsNullOrWhiteSpace(option.DescriptionHtml))
writer.Write($"<p class=\"hub-get-started-option-desc\">{option.DescriptionHtml}</p>");
if (!string.IsNullOrWhiteSpace(option.Code))
WriteCommand(writer, option.Code, option.Language);
else if (option.Commands.Count > 0)
{
var language = string.IsNullOrWhiteSpace(option.Language) ? "text" : option.Language;
writer.Write("<div class=\"hub-get-started-install highlight\"><pre><code class=\"language-");
writer.Write(System.Net.WebUtility.HtmlEncode(language));
writer.Write("\">");
writer.Write(System.Net.WebUtility.HtmlEncode(option.Code));
writer.Write("</code></pre></div>");
writer.Write("<div class=\"hub-get-started-commands\">");
foreach (var command in option.Commands)
{
if (!string.IsNullOrWhiteSpace(command.Label))
writer.Write($"<span class=\"hub-get-started-command-label\">{System.Net.WebUtility.HtmlEncode(command.Label)}</span>");
WriteCommand(writer, command.Code, option.Language);
}
writer.Write("</div>");
}
if (!string.IsNullOrWhiteSpace(option.Url))
{
if (!string.IsNullOrWhiteSpace(option.Code))
if (option.HasCode)
{
writer.Write("<a class=\"hub-get-started-option-link\" ");
writer.Write(Model.LinkAttributes(option.Url));
Expand All @@ -164,4 +168,13 @@
}
return new HtmlString(builder.ToString());
}

private static void WriteCommand(System.IO.TextWriter writer, string code, string? language)
{
writer.Write("<div class=\"hub-get-started-install highlight\"><pre><code class=\"language-");
writer.Write(System.Net.WebUtility.HtmlEncode(string.IsNullOrWhiteSpace(language) ? "text" : language));
writer.Write("\">");
writer.Write(System.Net.WebUtility.HtmlEncode(code));
writer.Write("</code></pre></div>");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,16 @@ public sealed record GetStartedOptionViewModel
public required string? Label { get; init; }
public required string? DescriptionHtml { get; init; }
public required string? Code { get; init; }
public required IReadOnlyList<GetStartedCommandViewModel> Commands { get; init; }
public required string? Language { get; init; }
public required string? Url { get; init; }
public required string? UrlLabel { get; init; }

public bool HasCode => !string.IsNullOrWhiteSpace(Code) || Commands.Count > 0;
}

public sealed record GetStartedCommandViewModel
{
public required string? Label { get; init; }
public required string Code { get; init; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,8 @@ private static void WriteGetStartedStep(LlmMarkdownRenderer renderer, GetStarted
renderer.WriteLine($" - {option.Label}: {option.Description}");
if (!string.IsNullOrEmpty(option.Code))
renderer.WriteLine($" `{option.Code}`");
foreach (var command in option.Commands)
renderer.WriteLine($" - {command.Label}: `{command.Code}`");
if (!string.IsNullOrEmpty(option.Url))
renderer.WriteLine($" [{option.UrlLabel ?? "Get started"}]({HubLinkForLlm(renderer, option.Url)})");
}
Expand Down
57 changes: 57 additions & 0 deletions tests/Elastic.Authoring.Tests/Blocks/Hub/GetStarted.cs
Original file line number Diff line number Diff line change
Expand Up @@ -260,3 +260,60 @@ public class GetStartedWithAFirstStepThatHasNoOptions : MarkdownTest
[Test, DisplayName("has no errors")]
public async Task HasNoErrors() => await Docs.HasNoErrors();
}

public class GetStartedWithLabeledCommands : MarkdownTest
{
protected override string Markdown =>
"""
:::{get-started}
title: Get started
steps:
- title: Run it
options:
- label: Run locally
commands:
- label: Server only
code: install --server-only
- label: Server and UI
code: install
language: sh
- label: Cloud
url: /index.md
:::
""";

[Test, DisplayName("labels each command")]
public async Task LabelsEachCommand() =>
await Docs.ConvertsToContainingRawHtml("""<span class="hub-get-started-command-label">Server and UI</span>""");

[Test, DisplayName("renders each command in its own code block")]
public async Task RendersEachCommandInItsOwnCodeBlock()
{
await Docs.ConvertsToContainingHtml("""<code class="language-sh">install --server-only</code>""");
await Docs.ConvertsToContainingHtml("""<code class="language-sh">install</code>""");
}

[Test, DisplayName("has no errors")]
public async Task HasNoErrors() => await Docs.HasNoErrors();
}

public class GetStartedWithCodeAndCommands : MarkdownTest
{
protected override string Markdown =>
"""
:::{get-started}
title: Get started
steps:
- title: Run it
options:
- label: Run locally
code: install
commands:
- label: Server only
code: install --server-only
:::
""";

[Test, DisplayName("rejects code and commands together")]
public async Task RejectsCodeAndCommandsTogether() => await Docs.HasError("either `code` or `commands`");
}
Original file line number Diff line number Diff line change
Expand Up @@ -488,3 +488,31 @@ public async Task Renders()
await Docs.DoesNotConvertToContainingHtml("<full-page-search");
}
}

public class GetStartedLabeledCommandsComponent : DesignSystemDirectiveTest
{
protected override string Markdown =>
"""
:::{get-started}
title: Get started
steps:
- title: Run it
options:
- label: Run locally
commands:
- label: Server only
code: install --server-only
- label: Server and UI
code: install
language: sh
:::
""";

[Test, DisplayName("gives each labeled command its own copy button")]
public async Task Renders()
{
await Docs.ConvertsToContainingRawHtml("""<span class="get-started__command-label">Server only</span>""");
await Docs.ConvertsToContainingRawHtml("data-copy-text=\"install --server-only\"");
await Docs.ConvertsToContainingRawHtml("data-copy-text=\"install\"");
}
}
Loading