diff --git a/docs/_snippets/applies_to-version.md b/docs/_snippets/applies_to-version.md
index f98f758d0..ef6ed62be 100644
--- a/docs/_snippets/applies_to-version.md
+++ b/docs/_snippets/applies_to-version.md
@@ -1,10 +1,65 @@
`applies_to` accepts the following version formats:
-* `Major.Minor`
-* `Major.Minor.Patch`
+### Version specifiers
-Regardless of the version format used in the source file, the version number is always rendered in the `Major.Minor.Patch` format.
+You can use version specifiers to precisely control how versions are interpreted:
+
+| Specifier | Syntax | Description | Example |
+|-----------|--------|-------------|---------|
+| Greater than or equal (default) | `x.x` `x.x+` `x.x.x` `x.x.x+` | Feature available from this version onwards | `ga 9.2+` or `ga 9.2` |
+| Range (inclusive) | `x.x-y.y` `x.x.x-y.y.y` | Feature available only in this version range | `beta 9.0-9.1` |
+| Exact version | `=x.x` `=x.x.x` | Feature available only in this specific version | `preview =9.0` |
+
+Regardless of the version format used in the source file, the version number is always rendered in the `Major.Minor` format in badges.
+
+:::{note}
+The `+` suffix is optional for greater-than-or-equal syntax. Both `ga 9.2` and `ga 9.2+` have the same meaning.
+:::
+
+### Examples
+
+```yaml
+# Greater than or equal (feature available from 9.2 onwards)
+stack: ga 9.2
+stack: ga 9.2+
+
+# Range (feature was in beta from 9.0 to 9.1, then became GA)
+stack: ga 9.2+, beta 9.0-9.1
+
+# Exact version (feature was in preview only in 9.0)
+stack: ga 9.1+, preview =9.0
+```
+
+### Implicit version inference for multiple lifecycles {#implicit-version-inference}
+
+When you specify multiple lifecycles with simple versions (without explicit specifiers), the system automatically infers the version ranges:
+
+**Input:**
+```yaml
+stack: preview 9.0, alpha 9.1, beta 9.2, ga 9.4
+```
+
+**Interpreted as:**
+```yaml
+stack: preview =9.0, alpha =9.1, beta 9.2-9.3, ga 9.4+
+```
+
+The inference rules are:
+1. **Consecutive versions**: If a lifecycle is immediately followed by another in the next minor version, it's treated as an **exact version** (`=x.x`).
+2. **Non-consecutive versions**: If there's a gap between one lifecycle's version and the next lifecycle's version, it becomes a **range** from the start version to one version before the next lifecycle.
+3. **Last lifecycle**: The highest versioned lifecycle is always treated as **greater-than-or-equal** (`x.x+`).
+
+This makes it easy to document features that evolve through multiple lifecycle stages. For example, a feature that goes through preview → beta → GA can be written simply as:
+
+```yaml
+stack: preview 9.0, beta 9.1, ga 9.3
+```
+
+Which is automatically interpreted as:
+```yaml
+stack: preview =9.0, beta 9.1-9.2, ga 9.3+
+```
:::{note}
-**Automatic Version Sorting**: When you specify multiple versions for the same product, the build system automatically sorts them in descending order (highest version first) regardless of the order you write them in the source file. For example, `stack: ga 8.18.6, ga 9.1.2, ga 8.19.2, ga 9.0.6` will be displayed as `stack: ga 9.1.2, ga 9.0.6, ga 8.19.2, ga 8.18.6`. Items without versions (like `ga` without a version or `all`) are sorted last.
+**Automatic Version Sorting**: When you specify multiple versions for the same product, the build system automatically sorts them in descending order (highest version first) regardless of the order you write them in the source file. For example, `stack: ga 9.1, beta 9.0, preview 8.18` will be displayed with the highest priority lifecycle and version first. Items without versions are sorted last.
:::
\ No newline at end of file
diff --git a/docs/syntax/_snippets/inline-level-applies-examples.md b/docs/syntax/_snippets/inline-level-applies-examples.md
index 58f38476c..1fedf4759 100644
--- a/docs/syntax/_snippets/inline-level-applies-examples.md
+++ b/docs/syntax/_snippets/inline-level-applies-examples.md
@@ -55,7 +55,7 @@ This example shows how to use directly a key from the second level of the `appli
::::{tab-item} Output
- {applies_to}`serverless: ga` {applies_to}`stack: ga 9.1.0`
-- {applies_to}`edot_python: preview 1.7.0, ga 1.8.0` {applies_to}`apm_agent_java: beta 1.0.0, ga 1.2.0`
+- {applies_to}`edot_python: preview =1.7.0, ga 1.8.0` {applies_to}`apm_agent_java: beta =1.0.0, ga 1.2.0`
- {applies_to}`stack: ga 9.0` {applies_to}`eck: ga 3.0`
::::
@@ -63,7 +63,7 @@ This example shows how to use directly a key from the second level of the `appli
::::{tab-item} Markdown
```markdown
- {applies_to}`serverless: ga` {applies_to}`stack: ga 9.1.0`
-- {applies_to}`edot_python: preview 1.7.0, ga 1.8.0` {applies_to}`apm_agent_java: beta 1.0.0, ga 1.2.0`
+- {applies_to}`edot_python: preview =1.7.0, ga 1.8.0` {applies_to}`apm_agent_java: beta =1.0.0, ga 1.2.0`
- {applies_to}`stack: ga 9.0` {applies_to}`eck: ga 3.0`
```
::::
diff --git a/docs/syntax/_snippets/multiple-lifecycle-states.md b/docs/syntax/_snippets/multiple-lifecycle-states.md
index bb0bedf40..8c9dcd069 100644
--- a/docs/syntax/_snippets/multiple-lifecycle-states.md
+++ b/docs/syntax/_snippets/multiple-lifecycle-states.md
@@ -1,12 +1,35 @@
-`applies_to` keys accept comma-separated values to specify lifecycle states for multiple product versions. For example:
+`applies_to` keys accept comma-separated values to specify lifecycle states for multiple product versions.
-* A feature is added in 9.1 as tech preview and becomes GA in 9.4:
+When you specify multiple lifecycles with simple versions, the system automatically infers whether each version represents an exact version, a range, or an open-ended range. Refer to [Implicit version inference](/_snippets/applies_to-version.md#implicit-version-inference) for details.
+
+### Examples
+
+* A feature is added in 9.0 as tech preview and becomes GA in 9.1:
+
+ ```yml
+ applies_to:
+ stack: preview 9.0, ga 9.1
+ ```
+
+ The preview is automatically interpreted as `=9.0` (exact), and GA as `9.1+` (open-ended).
+
+* A feature goes through multiple stages before becoming GA:
+
+ ```yml
+ applies_to:
+ stack: preview 9.0, beta 9.1, ga 9.3
+ ```
+
+ Interpreted as: `preview =9.0`, `beta 9.1-9.2`, `ga 9.3+`
+
+* A feature is unavailable for one version, beta for another, preview for a range, then GA:
```yml
applies_to:
- stack: preview 9.1, ga 9.4
+ stack: unavailable 9.0, beta 9.1, preview 9.2, ga 9.4
```
+ Interpreted as: `unavailable =9.0`, `beta =9.1`, `preview 9.2-9.3`, `ga 9.4+`
* A feature is deprecated in ECE 4.0 and is removed in 4.8. At the same time, it has already been removed in {{ech}}:
@@ -15,4 +38,17 @@
deployment:
ece: deprecated 4.0, removed 4.8
ess: removed
+ ```
+
+ The deprecated lifecycle is interpreted as `4.0-4.7` (range until removal).
+
+* Use explicit specifiers when you need precise control:
+
+ ```yml
+ applies_to:
+ # Explicit exact version
+ stack: preview =9.0, ga 9.1+
+
+ # Explicit range
+ stack: beta 9.0-9.1, ga 9.2+
```
\ No newline at end of file
diff --git a/docs/syntax/_snippets/versioned-lifecycle.md b/docs/syntax/_snippets/versioned-lifecycle.md
index ae6af6fee..cfe372e69 100644
--- a/docs/syntax/_snippets/versioned-lifecycle.md
+++ b/docs/syntax/_snippets/versioned-lifecycle.md
@@ -7,6 +7,8 @@
---
```
+ This means the feature is available from version 9.3 onwards (equivalent to `ga 9.3+`).
+
* When a change is introduced as preview or beta, use `preview` or `beta` as value for the corresponding key within the `applies_to`:
```
@@ -16,6 +18,28 @@
---
```
+* When a feature is available only in a specific version range, use the range syntax:
+
+ ```
+ ---
+ applies_to:
+ stack: beta 9.0-9.1, ga 9.2
+ ---
+ ```
+
+ This means the feature was in beta from 9.0 to 9.1, then became GA in 9.2+.
+
+* When a feature was in a specific lifecycle for exactly one version, use the exact syntax:
+
+ ```
+ ---
+ applies_to:
+ stack: preview =9.0, ga 9.1
+ ---
+ ```
+
+ This means the feature was in preview only in 9.0, then became GA in 9.1+.
+
* When a change introduces a deprecation, use `deprecated` as value for the corresponding key within the `applies_to`:
```
@@ -33,4 +57,6 @@
applies_to:
stack: deprecated 9.1, removed 9.4
---
- ```
\ No newline at end of file
+ ```
+
+ With the implicit version inference, this is interpreted as `deprecated 9.1-9.3, removed 9.4+`.
\ No newline at end of file
diff --git a/docs/syntax/applies.md b/docs/syntax/applies.md
index 52efe1aa5..90e5397ab 100644
--- a/docs/syntax/applies.md
+++ b/docs/syntax/applies.md
@@ -7,18 +7,17 @@ To support this, source files use a tagging system to indicate:
* Which Elastic products and deployment models the content applies to.
* When a feature changes state relative to the base version.
-This is what the `applies_to` metadata is for. It can be used at the [page](#page-level),
-[section](#section-level), or [inline](#inline-level) level to specify applicability with precision.
+This is what the `applies_to` metadata is for. It can be used at the [page](#page-level), [section](#section-level), or [inline](#inline-level) level to specify applicability with precision.
:::{note}
For detailed guidance, refer to [Write cumulative documentation](https://www.elastic.co/docs/contribute-docs/how-to/cumulative-docs).
:::
-## Syntax
+## Syntax reference
-The `applies_to` metadata supports an [exhaustive list of keys](#key-value-reference).
+The `applies_to` metadata supports an [exhaustive list of keys](#key-value-reference). When you write or edit documentation, only specify the keys that apply to that content.
-When you write or edit documentation, only specify the keys that apply to that content. Each key accepts values with the following syntax:
+Each key accepts values with the following syntax:
```
: [version], [version], ...
@@ -42,8 +41,6 @@ applies_to:
---
```
-For more examples, refer to [Page annotation examples](#page-annotation-examples).
-
:::{important}
All documentation pages must include an `applies_to` tag in the YAML frontmatter.
:::
@@ -70,8 +67,6 @@ stack: ga 9.1
This allows the YAML inside the `{applies_to}` directive to be fully highlighted.
-For more examples, refer to [Section annotation examples](#section-annotation-examples).
-
:::{note}
Section-level `{applies_to}` directives must be preceded by a heading directly.
:::
@@ -84,16 +79,13 @@ You can add inline applies annotations to any line using the following syntax:
This can live inline {applies_to}`section: [version]`
```
-A specialized `{preview}` role exists to quickly mark something as a technical preview. It takes a required version number
-as an argument.
+A specialized `{preview}` role exists to quickly mark something as a technical preview. It takes a required version number as an argument.
```markdown
Property {preview}``
: definition body
```
-For more examples, refer to [Inline annotation examples](#inline-annotation-examples).
-
### On specific components
Several components have built-in support for `applies_to` and allow to surface version information in an optimized way:
@@ -104,6 +96,60 @@ Several components have built-in support for `applies_to` and allow to surface v
Refer to these component pages to learn about the required `applies_to` syntax.
+## Version syntax
+
+Versions can be specified using several formats to indicate different applicability scenarios.
+
+### Formats
+
+| Format | Syntax | Example | Badge Display | Description |
+|:-------|:-------|:--------|:--------------|:------------|
+| **Greater than or equal to** (default) | `x.x+` `x.x` `x.x.x+` `x.x.x` | `ga 9.1` or `ga 9.1+` | `9.1+` | Applies from this version onwards |
+| **Range** (inclusive) | `x.x-y.y` `x.x.x-y.y.y` | `preview 9.0-9.2` | `9.0-9.2` or `9.0+`* | Applies within the specified range |
+| **Exact version** | `=x.x` `=x.x.x` | `beta =9.1` | `9.1` | Applies only to this specific version |
+
+\* Range display depends on release status of the second version.
+
+**Important notes:**
+
+- Versions are always displayed as **Major.Minor** (e.g., `9.1`) in badges, regardless of whether you specify patch versions in the source.
+- Each version statement corresponds to the **latest patch** of the specified minor version (e.g., `9.1` represents 9.1.0, 9.1.1, 9.1.6, etc.).
+- When critical patch-level differences exist, use plain text descriptions alongside the badge rather than specifying patch versions.
+
+### Rendered examples
+
+The following table shows how different version syntaxes render:
+
+| Rendered | Raw input | Notes |
+|----------|-----------|-------|
+| {applies_to}`stack: ga 9.1` | `` {applies_to}`stack: ga 9.1` `` | Implicit `+` (default behavior) |
+| {applies_to}`stack: ga 9.1+` | `` {applies_to}`stack: ga 9.1+` `` | Explicit `+` |
+| {applies_to}`stack: preview 9.0+` | `` {applies_to}`stack: preview 9.0+` `` | Preview with version |
+| {applies_to}`stack: preview 9.0-9.2` | `` {applies_to}`stack: preview 9.0-9.2` `` | Range display when both ends are released |
+| {applies_to}`stack: beta 9.1-9.3` | `` {applies_to}`stack: beta 9.1-9.3` `` | Converts to `+` if the end version is unreleased |
+| {applies_to}`stack: beta =9.1` | `` {applies_to}`stack: beta =9.1` `` | Exact version (no `+` symbol) |
+| {applies_to}`stack: deprecated =9.0` | `` {applies_to}`stack: deprecated =9.0` `` | Deprecated exact version |
+| {applies_to}`stack: ga 9.2+, beta 9.0-9.1` | `` {applies_to}`stack: ga 9.2+, beta 9.0-9.1` `` | Multiple lifecycles (highest priority shown) |
+| {applies_to}`stack: ga 9.3, beta 9.1+` | `` {applies_to}`stack: ga 9.3, beta 9.1+` `` | Shows Beta when GA is unreleased (2+ lifecycles) |
+| {applies_to}`serverless: ga` | `` {applies_to}`serverless: ga` `` | No version badge for unversioned products |
+
+### Validation rules
+
+The build process enforces the following validation rules:
+
+- **One version per lifecycle**: Each lifecycle (GA, Preview, Beta, etc.) can only have one version declaration.
+ - ✅ `stack: ga 9.2+, beta 9.0-9.1`
+ - ❌ `stack: ga 9.2, ga 9.3`
+- **One "greater than" per key**: Only one lifecycle per product key can use the `+` (greater than or equal to) syntax.
+ - ✅ `stack: ga 9.2+, beta 9.0-9.1`
+ - ❌ `stack: ga 9.2+, beta 9.0+`
+- **Valid range order**: In ranges, the first version must be less than or equal to the second version.
+ - ✅ `stack: preview 9.0-9.2`
+ - ❌ `stack: preview 9.2-9.0`
+- **No version overlaps**: Versions for the same key cannot overlap (ranges are inclusive).
+ - ✅ `stack: ga 9.2+, beta 9.0-9.1`
+ - ❌ `stack: ga 9.2+, beta 9.0-9.2`
+
## Key-value reference
Use the following key-value reference to find the appropriate key and value for your applicability statements.
@@ -134,63 +180,142 @@ Use the following key-value reference to find the appropriate key and value for
## Examples
-### Versioning examples
+### By scope
+
+:::::{tab-set}
+
+::::{tab-item} Page level
+
+:::{include} _snippets/page-level-applies-examples.md
+:::
+
+::::
+
+::::{tab-item} Section level
+
+:::{include} _snippets/section-level-applies-examples.md
+:::
+
+::::
+
+::::{tab-item} Inline level
+
+:::{include} _snippets/inline-level-applies-examples.md
+:::
+
+::::
+
+:::::
+
+### Versioned vs unversioned products
+
+:::::{tab-set}
+
+::::{tab-item} Versioned products
Versioned products require a `version` tag to be used with the `lifecycle` tag:
-```
+```yaml
applies_to:
stack: preview 9.1, ga 9.4
deployment:
ece: deprecated 9.2, removed 9.8
```
+:::{include} _snippets/versioned-lifecycle.md
+:::
+
+::::
+
+::::{tab-item} Unversioned products
+
Unversioned products use `lifecycle` tags without a version:
-```
+```yaml
applies_to:
serverless:
elasticsearch: beta
observability: removed
```
-### Lifecycle and versioning examples
-
-:::::{dropdown} Unversioned products
-
:::{include} _snippets/unversioned-lifecycle.md
:::
-:::::
+::::
-:::::{dropdown} Versioned products
+::::{tab-item} Multiple lifecycle states
-:::{include} _snippets/versioned-lifecycle.md
+:::{include} _snippets/multiple-lifecycle-states.md
:::
+::::
+
:::::
-:::::{dropdown} Identify multiple states for the same content
+### Inline examples by product
-:::{include} /syntax/_snippets/multiple-lifecycle-states.md
-:::
+:::::{tab-set}
+
+::::{tab-item} Stack
+
+| `applies_to` | Result |
+|--------------|--------|
+| `` {applies_to}`stack: ` `` | {applies_to}`stack: ` |
+| `` {applies_to}`stack: preview` `` | {applies_to}`stack: preview` |
+| `` {applies_to}`stack: preview 8.18` `` | {applies_to}`stack: preview 8.18` |
+| `` {applies_to}`stack: preview 9.0` `` | {applies_to}`stack: preview 9.0` |
+| `` {applies_to}`stack: preview 9.1` `` | {applies_to}`stack: preview 9.1` |
+| `` {applies_to}`stack: ga` `` | {applies_to}`stack: ga` |
+| `` {applies_to}`stack: ga 8.18` `` | {applies_to}`stack: ga 8.18` |
+| `` {applies_to}`stack: ga 9.0` `` | {applies_to}`stack: ga 9.0` |
+| `` {applies_to}`stack: ga 9.1` `` | {applies_to}`stack: ga 9.1` |
+| `` {applies_to}`stack: beta` `` | {applies_to}`stack: beta` |
+| `` {applies_to}`stack: beta 9.0` `` | {applies_to}`stack: beta 9.0` |
+| `` {applies_to}`stack: deprecated` `` | {applies_to}`stack: deprecated` |
+| `` {applies_to}`stack: deprecated 9.0` `` | {applies_to}`stack: deprecated 9.0` |
+| `` {applies_to}`stack: removed` `` | {applies_to}`stack: removed` |
+| `` {applies_to}`stack: removed 9.0` `` | {applies_to}`stack: removed 9.0` |
+
+::::
+
+::::{tab-item} Serverless
+
+| `applies_to` | Result |
+|--------------|--------|
+| `` {applies_to}`serverless: ` `` | {applies_to}`serverless: ` |
+| `` {applies_to}`serverless: preview` `` | {applies_to}`serverless: preview` |
+| `` {applies_to}`serverless: ga` `` | {applies_to}`serverless: ga` |
+| `` {applies_to}`serverless: beta` `` | {applies_to}`serverless: beta` |
+| `` {applies_to}`serverless: deprecated` `` | {applies_to}`serverless: deprecated` |
+| `` {applies_to}`serverless: removed` `` | {applies_to}`serverless: removed` |
+
+::::
:::::
-### Page annotation examples
+### Block example
-:::{include} _snippets/page-level-applies-examples.md
-:::
+```{applies_to}
+stack: preview 9.1+
+serverless: ga
-### Section annotation examples
+apm_agent_dotnet: ga 1.0+
+apm_agent_java: beta 1.0+
+edot_dotnet: preview 1.0+
+edot_python:
+edot_node: ga 1.0+
+elasticsearch: preview 9.0+
+security: removed 9.0
+observability: deprecated 9.0+
+```
-:::{include} _snippets/section-level-applies-examples.md
-:::
+### In-text example
-### Inline annotation examples
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas ut libero diam. Mauris sed eleifend erat, sit amet auctor odio. Donec ac placerat nunc. {applies_to}`stack: preview` Aenean scelerisque viverra lectus nec dignissim.
-:::{include} _snippets/inline-level-applies-examples.md
-:::
+- {applies_to}`elasticsearch: preview` Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+- {applies_to}`observability: preview` Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+- {applies_to}`security: preview` Lorem ipsum dolor sit amet, consectetur adipiscing elit.
## Structured model
@@ -238,97 +363,223 @@ applies_to:
```
:::::
-## Look and feel
+## Badge rendering reference
-### Block
+This section provides detailed rules for how badges are rendered based on lifecycle, version, and release status. Use this as a reference when you need to understand the exact rendering behavior.
-:::::{dropdown} Block examples
+### Rendering order
-```{applies_to}
-stack: preview 9.1
-serverless: ga
+`applies_to` badges are displayed in a consistent order regardless of how they appear in your source files:
+
+1. **Stack** - Elastic Stack
+2. **Serverless** - Elastic Cloud Serverless offerings
+3. **Deployment** - Deployment options (ECH, ECK, ECE, Self-managed)
+4. **ProductApplicability** - Specialized tools and agents (ECCTL, Curator, EDOT, APM Agents)
+5. **Product (generic)** - Generic product applicability
+
+Within the ProductApplicability category, EDOT and APM Agent items are sorted alphabetically for better scanning.
+
+:::{note}
+Inline applies annotations are rendered in the order they appear in the source file.
+:::
+
+### Badge rendering rules
+
+:::::{dropdown} No version declared (Serverless)
+
+| Lifecycle | Release status | Lifecycle count | Rendered output |
+|:----------|:---------------|-----------------|:----------------|
+| GA | – | – | `{product}` |
+| Preview | – | – | `{product}\|Preview` |
+| Beta | – | – | `{product}\|Beta` |
+| Deprecated | – | – | `{product}\|Deprecated` |
+| Removed | – | – | `{product}\|Removed` |
+| Unavailable | – | – | `{product}\|Unavailable` |
-apm_agent_dotnet: ga 1.0.0
-apm_agent_java: beta 1.0.0
-edot_dotnet: preview 1.0.0
-edot_python:
-edot_node: ga 1.0.0
-elasticsearch: preview 9.0.0
-security: removed 9.0.0
-observability: deprecated 9.0.0
-```
:::::
-### Inline
+:::::{dropdown} No version declared (Other versioning systems)
-:::::{dropdown} In text
+| Lifecycle | Release status | Lifecycle count | Rendered output |
+|:----------|:---------------|-----------------|:----------------|
+| GA | – | – | `{product}\|{base}+` |
+| Preview | – | – | `{product}\|Preview {base}+` |
+| Beta | – | – | `{product}\|Beta {base}+` |
+| Deprecated | – | – | `{product}\|Deprecated {base}+` |
+| Removed | – | – | `{product}\|Removed {base}+` |
+| Unavailable | – | – | `{product}\|Unavailable {base}+` |
-Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas ut libero diam. Mauris sed eleifend erat,
-sit amet auctor odio. Donec ac placerat nunc. {applies_to}`stack: preview` Aenean scelerisque viverra lectus
-nec dignissim. Vestibulum ut felis nec massa auctor placerat. Maecenas vel dictum.
+:::::
-- {applies_to}`elasticsearch: preview` Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas ut libero diam. Mauris sed eleifend erat, sit amet auctor odio. Donec ac placerat nunc.
-- {applies_to}`observability: preview` Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas ut libero diam.
-- {applies_to}`security: preview` Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas ut libero diam. Mauris sed eleifend erat, sit amet auctor odio. Donec ac placerat nunc. Aenean scelerisque viverra lectus nec dignissim.
+:::::{dropdown} Greater than or equal to (x.x+, x.x, x.x.x+, x.x.x)
+
+| Lifecycle | Release status | Lifecycle count | Rendered output |
+|:----------|:---------------|-----------------|:----------------|
+| GA | Released | \>= 1 | `{product}\|x.x+` |
+| | Unreleased | 1 | `{product}\|Planned` |
+| | | \>= 2 | Use previous lifecycle |
+| Preview | Released | \>= 1 | `{product}\|Preview x.x+` |
+| | Unreleased | 1 | `{product}\|Planned` |
+| | | \>= 2 | Use previous lifecycle |
+| Beta | Released | \>= 1 | `{product}\|Beta x.x+` |
+| | Unreleased | 1 | `{product}\|Planned` |
+| | | \>= 2 | Use previous lifecycle |
+| Deprecated | Released | \>= 1 | `{product}\|Deprecated x.x+` |
+| | Unreleased | 1 | `{product}\|Deprecation planned` |
+| | | \>= 2 | Use previous lifecycle |
+| Removed | Released | \>= 1 | `{product}\|Removed x.x` |
+| | Unreleased | 1 | `{product}\|Removal planned` |
+| | | \>= 2 | Use previous lifecycle |
:::::
-:::::{dropdown} Stack
-
-| `applies_to` | result |
-|--------------------------------------------|--------------------------------------|
-| `` {applies_to}`stack: ` `` | {applies_to}`stack: ` |
-| `` {applies_to}`stack: preview` `` | {applies_to}`stack: preview` |
-| `` {applies_to}`stack: preview 8.18` `` | {applies_to}`stack: preview 8.18` |
-| `` {applies_to}`stack: preview 9.0` `` | {applies_to}`stack: preview 9.0` |
-| `` {applies_to}`stack: preview 9.1` `` | {applies_to}`stack: preview 9.1` |
-| `` {applies_to}`stack: preview 99.0` `` | {applies_to}`stack: preview 99.0` |
-| `` {applies_to}`stack: ga` `` | {applies_to}`stack: ga` |
-| `` {applies_to}`stack: ga 8.18` `` | {applies_to}`stack: ga 8.18` |
-| `` {applies_to}`stack: ga 9.0` `` | {applies_to}`stack: ga 9.0` |
-| `` {applies_to}`stack: ga 9.1` `` | {applies_to}`stack: ga 9.1` |
-| `` {applies_to}`stack: ga 99.0` `` | {applies_to}`stack: ga 99.0` |
-| `` {applies_to}`stack: beta` `` | {applies_to}`stack: beta` |
-| `` {applies_to}`stack: beta 8.18` `` | {applies_to}`stack: beta 8.18` |
-| `` {applies_to}`stack: beta 9.0` `` | {applies_to}`stack: beta 9.0` |
-| `` {applies_to}`stack: beta 9.1` `` | {applies_to}`stack: beta 9.1` |
-| `` {applies_to}`stack: beta 99.0` `` | {applies_to}`stack: beta 99.0` |
-| `` {applies_to}`stack: deprecated` `` | {applies_to}`stack: deprecated` |
-| `` {applies_to}`stack: deprecated 8.18` `` | {applies_to}`stack: deprecated 8.18` |
-| `` {applies_to}`stack: deprecated 9.0` `` | {applies_to}`stack: deprecated 9.0` |
-| `` {applies_to}`stack: deprecated 9.1` `` | {applies_to}`stack: deprecated 9.1` |
-| `` {applies_to}`stack: deprecated 99.0` `` | {applies_to}`stack: deprecated 99.0` |
-| `` {applies_to}`stack: removed` `` | {applies_to}`stack: removed` |
-| `` {applies_to}`stack: removed 8.18` `` | {applies_to}`stack: removed 8.18` |
-| `` {applies_to}`stack: removed 9.0` `` | {applies_to}`stack: removed 9.0` |
-| `` {applies_to}`stack: removed 9.1` `` | {applies_to}`stack: removed 9.1` |
-| `` {applies_to}`stack: removed 99.0` `` | {applies_to}`stack: removed 99.0` |
+:::::{dropdown} Range (x.x-y.y, x.x.x-y.y.y)
+
+| Lifecycle | Release status | Lifecycle count | Rendered output |
+|:----------|:---------------|-----------------|:----------------|
+| GA | `y.y.y` is released | \>= 1 | `{product}\|x.x-y.y` |
+| | `y.y.y` is **not** released, `x.x.x` is released | \>= 1 | `{product}\|x.x+` |
+| | `y.y.y` is **not** released, `x.x.x` is **not** released | 1 | `{product}\|Planned` |
+| | | \>= 2 | Use previous lifecycle |
+| Preview | `y.y.y` is released | \>= 1 | `{product}\|Preview x.x-y.y` |
+| | `y.y.y` is **not** released, `x.x.x` is released | \>= 1 | `{product}\|Preview x.x+` |
+| | `y.y.y` is **not** released, `x.x.x` is **not** released | 1 | `{product}\|Planned` |
+| | | \>= 2 | Use previous lifecycle |
+| Beta | `y.y.y` is released | \>= 1 | `{product}\|Beta x.x-y.y` |
+| | `y.y.y` is **not** released, `x.x.x` is released | \>= 1 | `{product}\|Beta x.x+` |
+| | `y.y.y` is **not** released, `x.x.x` is **not** released | 1 | `{product}\|Planned` |
+| | | \>= 2 | Use previous lifecycle |
+| Deprecated | `y.y.y` is released | \>= 1 | `{product}\|Deprecated x.x-y.y` |
+| | `y.y.y` is **not** released, `x.x.x` is released | \>= 1 | `{product}\|Deprecated x.x+` |
+| | `y.y.y` is **not** released, `x.x.x` is **not** released | \>= 1 | `{product}\|Deprecation planned` |
+| Removed | `y.y.y` is released | \>= 1 | `{product}\|Removed x.x` |
+| | `y.y.y` is **not** released, `x.x.x` is released | \>= 1 | `{product}\|Removed x.x` |
+| | `y.y.y` is **not** released, `x.x.x` is **not** released | \>= 1 | `{product}\|Removal planned` |
+| Unavailable | `y.y.y` is released | \>= 1 | `{product}\|Unavailable X.X-Y.Y` |
+| | `y.y.y` is **not** released, `x.x.x` is released | \>= 1 | `{product}\|Unavailable X.X+` |
+| | `y.y.y` is **not** released, `x.x.x` is **not** released | \>= 1 | ??? |
+
:::::
-:::::{dropdown} Serverless
+:::::{dropdown} Exact version (=x.x, =x.x.x)
+
+| Lifecycle | Release status | Lifecycle count | Rendered output |
+|:----------|:---------------|-----------------|:----------------|
+| GA | Released | \>= 1 | `{product}\|X.X` |
+| | Unreleased | 1 | `{product}\|Planned` |
+| | | \>= 2 | Use previous lifecycle |
+| Preview | Released | \>= 1 | `{product}\|Preview X.X` |
+| | Unreleased | 1 | `{product}\|Planned` |
+| | | \>= 2 | Use previous lifecycle |
+| Beta | Released | \>= 1 | `{product}\|Beta X.X` |
+| | Unreleased | 1 | `{product}\|Planned` |
+| | | \>= 2 | Use previous lifecycle |
+| Deprecated | Released | \>= 1 | `{product}\|Deprecated X.X` |
+| | Unreleased | \>= 1 | `{product}\|Deprecation planned` |
+| Removed | Released | \>= 1 | `{product}\|Removed X.X` |
+| | Unreleased | \>=1 | `{product}\|Removal planned` |
+| Unavailable | Released | \>= 1 | `{product}\|Unavailable X.X` |
+| | Unreleased | \>= 1 | ??? |
-| `applies_to` | result |
-|-------------------------------------------------|-------------------------------------------|
-| `` {applies_to}`serverless: ` `` | {applies_to}`serverless: ` |
-| `` {applies_to}`serverless: preview` `` | {applies_to}`serverless: preview` |
-| `` {applies_to}`serverless: ga` `` | {applies_to}`serverless: ga` |
-| `` {applies_to}`serverless: beta` `` | {applies_to}`serverless: beta` |
-| `` {applies_to}`serverless: deprecated` `` | {applies_to}`serverless: deprecated` |
-| `` {applies_to}`serverless: removed` `` | {applies_to}`serverless: removed` |
:::::
-### Badge rendering order
+### Popover availability text
-`applies_to` badges are displayed in a consistent order regardless of how they appear in your source files. This ensures users always see badges in a predictable hierarchy:
+:::::{dropdown} No version declared (Serverless)
-1. **Stack** - Elastic Stack
-2. **Serverless** - Elastic Cloud Serverless offerings
-3. **Deployment** - Deployment options (ECH, ECK, ECE, Self-managed)
-4. **ProductApplicability** - Specialized tools and agents (ECCTL, Curator, EDOT, APM Agents)
-5. **Product (generic)** - Generic product applicability
+| Lifecycle | Release status | Lifecycle count | Rendered output |
+|:----------|:---------------|-----------------|:----------------|
+| GA | – | 1 | `Generally available` |
+| Preview | – | 1 | `Preview` |
+| Beta | – | 1 | `Beta` |
+| Deprecated | – | 1 | `Deprecated` |
+| Removed | – | 1 | `Removed` |
+| Unavailable | – | 1 | `Unavailable` |
-Within the ProductApplicability category, EDOT and APM Agent items are sorted alphabetically for better scanning.
+:::::
-:::{note}
-Inline applies annotations are rendered in the order they appear in the source file.
-:::
\ No newline at end of file
+:::::{dropdown} No version declared (Other versioning systems)
+
+| Lifecycle | Release status | Lifecycle count | Rendered output |
+|:----------|:---------------|-----------------|:----------------|
+| GA | – | 1 | `Generally available since {base}` |
+| Preview | – | 1 | `Preview since {base}` |
+| Beta | – | 1 | `Beta since {base}` |
+| Deprecated | – | 1 | `Deprecated since {base}` |
+| Removed | – | 1 | `Removed in {base}` |
+| Unavailable | – | 1 | `Unavailable since {base}` |
+
+:::::
+
+:::::{dropdown} Greater than or equal to (x.x+, x.x, x.x.x+, x.x.x)
+
+| Lifecycle | Release status | Lifecycle count | Rendered output |
+|:----------|:---------------|-----------------|:----------------|
+| GA | Released | \>= 1 | `Generally available since X.X` |
+| | Unreleased | 1 | `Planned` |
+| | | \>= 2 | Do not add to availability list |
+| Preview | Released | \>= 1 | `Preview since X.X` |
+| | Unreleased | 1 | `Planned` |
+| | | \>= 2 | Do not add to availability list |
+| Beta | Released | \>= 1 | `Beta since X.X` |
+| | Unreleased | 1 | `Planned` |
+| | | \>= 2 | Do not add to availability list |
+| Deprecated | Released | \>= 1 | `Deprecated since X.X` |
+| | Unreleased | \>= 1 | `Planned for deprecation` |
+| Removed | Released | \>= 1 | `Removed in X.X` |
+| | Unreleased | \>=1 | `Planned for removal` |
+| Unavailable | Released | \>= 1 | `Unavailable since X.X` |
+| | Unreleased | 1 | `Unavailable` |
+| | | \>= 2 | Do not add to availability list |
+
+:::::
+
+:::::{dropdown} Range (x.x-y.y, x.x.x-y.y.y)
+
+| Lifecycle | Release status | Lifecycle count | Rendered output |
+|:----------|:---------------|-----------------|:----------------|
+| GA | `y.y.y` is released | \>= 1 | `Generally available from X.X to Y.Y` |
+| | `y.y.y` is **not** released, `x.x.x` is released | \>= 1 | `Generally available since X.X` |
+| | `y.y.y` is **not** released, `x.x.x` is **not** released | 1 | `Planned` |
+| | | \>= 2 | Do not add to availability list |
+| Preview | `y.y.y` is released | \>= 1 | `Preview from X.X to Y.Y` |
+| | `y.y.y` is **not** released, `x.x.x` is released | \>= 1 | `Preview since X.X` |
+| | `y.y.y` is **not** released, `x.x.x` is **not** released | 1 | `Planned` |
+| | | \>= 2 | Do not add to availability list |
+| Beta | `y.y.y` is released | \>= 1 | `Beta from X.X to Y.Y` |
+| | `y.y.y` is **not** released, `x.x.x` is released | \>= 1 | `Beta since X.X` |
+| | `y.y.y` is **not** released, `x.x.x` is **not** released | 1 | `Planned` |
+| | | \>= 2 | Do not add to availability list |
+| Deprecated | `y.y.y` is released | \>= 1 | `Deprecated from X.X to Y.Y` |
+| | `y.y.y` is **not** released, `x.x.x` is released | \>= 1 | `Deprecated since X.X` |
+| | `y.y.y` is **not** released, `x.x.x` is **not** released | \>= 1 | `Planned for deprecation` |
+| Removed | `y.y.y` is released | \>= 1 | `Removed in X.X` |
+| | `y.y.y` is **not** released, `x.x.x` is released | \>= 1 | `Removed in X.X` |
+| | `y.y.y` is **not** released, `x.x.x` is **not** released | \>= 1 | `Planned for removal` |
+| Unavailable | `y.y.y` is released | \>= 1 | `Unavailable from X.X to Y.Y` |
+| | `y.y.y` is **not** released, `x.x.x` is released | \>= 1 | `Unavailable since X.X` |
+| | `y.y.y` is **not** released, `x.x.x` is **not** released | \>= 1 | Do not add to availability list |
+
+:::::
+
+:::::{dropdown} Exact version (=x.x, =x.x.x)
+
+| Lifecycle | Release status | Lifecycle count | Rendered output |
+|:----------|:---------------|-----------------|:----------------|
+| GA | Released | \>= 1 | `Generally available in X.X` |
+| | Unreleased | 1 | `Planned` |
+| | | \>= 2 | Do not add to availability list |
+| Preview | Released | \>= 1 | `Preview in X.X` |
+| | Unreleased | 1 | `Planned` |
+| | | \>= 2 | Do not add to availability list |
+| Beta | Released | \>= 1 | `Beta in X.X` |
+| | Unreleased | 1 | `Planned` |
+| | | \>= 2 | Do not add to availability list |
+| Deprecated | Released | \>= 1 | `Deprecated in X.X` |
+| | Unreleased | \>= 1 | `Planned for deprecation` |
+| Removed | Released | \>= 1 | `Removed in X.X` |
+| | Unreleased | \>=1 | `Planned for removal` |
+| Unavailable | Released | \>= 1 | `Unavailable in X.X` |
+| | Unreleased | \>= 1 | Do not add to availability list |
+
+:::::
diff --git a/docs/testing/req.md b/docs/testing/req.md
index 95907215f..39bc9004c 100644
--- a/docs/testing/req.md
+++ b/docs/testing/req.md
@@ -8,24 +8,131 @@ mapped_pages:
---
# Requirements
+This page demonstrates various `applies_to` version syntax examples.
+
+## Version specifier examples
+
+### Greater than or equal (default)
+
+```{applies_to}
+stack: ga 9.0
+```
+
+This is equivalent to `ga 9.0+` — the feature is available from version 9.0 onwards.
+
+### Explicit range
+
+```{applies_to}
+stack: beta 9.0-9.1, ga 9.2
+```
+
+The feature was in beta from 9.0 to 9.1 (inclusive), then became GA in 9.2+.
+
+### Exact version
+
+```{applies_to}
+stack: preview =9.0, ga 9.1
+```
+
+The feature was in preview only in version 9.0 (exactly), then became GA in 9.1+.
+
+## Implicit version inference examples
+
+### Simple two-stage lifecycle
+
```{applies_to}
stack: preview 9.0, ga 9.1
```
-1. Select **Create** to create a new policy, or select **Edit** {icon}`pencil` to open an existing policy.
-1. Select **Create** to create a new policy, or select **Edit** {icon}`logo_vulnerability_management` to open an existing policy.
+Interpreted as: `preview =9.0` (exact), `ga 9.1+` (open-ended).
+### Multi-stage lifecycle with consecutive versions
-{applies_to}`stack: preview 9.0` This tutorial is based on Elasticsearch 9.0.
-This tutorial is based on Elasticsearch 9.0. This tutorial is based on Elasticsearch 9.0.
-This tutorial is based on Elasticsearch 9.0.
+```{applies_to}
+stack: preview 9.0, beta 9.1, ga 9.2
+```
-what
+Interpreted as: `preview =9.0`, `beta =9.1`, `ga 9.2+`.
+### Multi-stage lifecycle with gaps
-To follow this tutorial you will need to install the following components:
+```{applies_to}
+stack: unavailable 9.0, beta 9.1, preview 9.2, ga 9.4
+```
+
+Interpreted as: `unavailable =9.0`, `beta =9.1`, `preview 9.2-9.3` (range to fill the gap), `ga 9.4+`.
+
+### Three stages with varying gaps
+
+```{applies_to}
+stack: preview 8.0, beta 9.1, ga 9.3
+```
+
+Interpreted as: `preview 8.0-8.19`, `beta 9.0-9.1`, `ga 9.2+`.
+
+## Inline examples
+
+{applies_to}`stack: preview 9.0` This feature is in preview in 9.0.
+
+{applies_to}`stack: beta 9.0-9.1` This feature was in beta from 9.0 to 9.1.
+
+{applies_to}`stack: ga 9.2+` This feature is generally available since 9.2.
+
+{applies_to}`stack: preview =9.0` This feature was in preview only in 9.0 (exact).
+
+## Explicit patch version display
+By default, patch versions (e.g., the `.4` in `9.0.4`) are hidden in badge displays, showing only `9.0` instead. To explicitly show the patch version, add an exclamation mark `!` after the version declaration.
+### Single version with explicit patch
+
+{applies_to}`stack: preview 7.5.4!` Shows `7.5.4+` instead of `7.5+`.
+
+{applies_to}`stack: preview 7.5.4` Without `!`, shows `7.5+` (patch hidden).
+
+### Range versions with explicit patch
+
+{applies_to}`stack: beta 7.0.3!-7.5.2!` Shows `7.0.3-7.5.2` with both patch versions visible.
+
+{applies_to}`stack: ga 7.0.5!-7.5` Shows `7.0.5-7.5` with patch only on min.
+
+{applies_to}`stack: ga 7.0-7.5.3!` Shows `7.0-7.5.3` with patch only on max.
+
+### Exact version with explicit patch
+
+{applies_to}`stack: ga =7.5.3!` Shows `7.5.3` as an exact version with patch visible.
+
+## Deprecation and removal examples
+
+```{applies_to}
+stack: deprecated 9.2, removed 9.5
+```
+
+Interpreted as: `deprecated 9.2-9.4`, `removed 9.5+`.
+
+{applies_to}`stack: deprecated 9.0` This feature is deprecated starting in 9.0.
+
+{applies_to}`stack: removed 9.2` This feature was removed in 9.2.
+
+## Mixed deployment examples
+
+```{applies_to}
+stack: ga 9.0
+deployment:
+ ece: ga 4.0
+ eck: beta 3.0, ga 3.1
+```
+
+### Handling multiple future versions
+
+```{applies_to}
+eck: beta 3.4, ga 3.5, deprecated 3.9
+```
+
+
+## Additional content
+
+To follow this tutorial you will need to install the following components:
- An installation of Elasticsearch, based on our hosted [Elastic Cloud](https://www.elastic.co/cloud) service (which includes a free trial period), or a self-hosted service that you run on your own computer. See the Install Elasticsearch section above for installation instructions.
- A [Python](https://python.org) interpreter. Make sure it is a recent version, such as Python 3.8 or newer.
@@ -36,5 +143,20 @@ The tutorial assumes that you have no previous knowledge of Elasticsearch or gen
- The [Flask](https://flask.palletsprojects.com/) web framework for Python.
- The command prompt or terminal application in your operating system.
-
{applies_to}`ece: removed`
+
+{applies_to}`ece: `
+
+{applies_to}`stack: deprecated 7.16.0, removed 8.0.0`
+
+{applies_to}`ess: `
+
+{applies_to}`stack: preview 9.0, ga 9.2, deprecated 9.7`
+
+{applies_to}`stack: preview 9.0, removed 9.1`
+
+{applies_to}`stack: preview 9.0.0-9.0.3, removed 9.3`
+
+{applies_to}`stack: preview 9.0, ga 9.4, removed 9.7`
+
+{applies_to}`stack: preview 9.0, deprecated 9.4, removed 9.7`
diff --git a/src/Elastic.ApiExplorer/Elasticsearch/OpenApiDocumentExporter.cs b/src/Elastic.ApiExplorer/Elasticsearch/OpenApiDocumentExporter.cs
index b2c82a1ec..d94f5ca7c 100644
--- a/src/Elastic.ApiExplorer/Elasticsearch/OpenApiDocumentExporter.cs
+++ b/src/Elastic.ApiExplorer/Elasticsearch/OpenApiDocumentExporter.cs
@@ -209,7 +209,7 @@ private bool ShouldIncludeOperation(OpenApiOperation operation, string product)
return true; // Could not parse version, safe to include
// Get current version for the product
- var versioningSystemId = product == "elasticsearch"
+ var versioningSystemId = product.Equals("elasticsearch", StringComparison.OrdinalIgnoreCase)
? VersioningSystemId.Stack
: VersioningSystemId.Stack; // Both use Stack for now
@@ -294,14 +294,14 @@ private static ProductLifecycle ParseLifecycle(string stateValue)
///
/// Parses the version from "Added in X.Y.Z" pattern in the x-state string.
///
- private static SemVersion? ParseVersion(string stateValue)
+ private static VersionSpec? ParseVersion(string stateValue)
{
var match = AddedInVersionRegex().Match(stateValue);
if (!match.Success)
return null;
var versionString = match.Groups[1].Value;
- return SemVersion.TryParse(versionString, out var version) ? version : null;
+ return VersionSpec.TryParse(versionString, out var version) ? version : null;
}
///
diff --git a/src/Elastic.Documentation.Configuration/Versions/VersionConfiguration.cs b/src/Elastic.Documentation.Configuration/Versions/VersionConfiguration.cs
index 717ecdc17..8ff0a3ed2 100644
--- a/src/Elastic.Documentation.Configuration/Versions/VersionConfiguration.cs
+++ b/src/Elastic.Documentation.Configuration/Versions/VersionConfiguration.cs
@@ -158,6 +158,7 @@ public record VersioningSystem
[YamlMember(Alias = "current")]
public required SemVersion Current { get; init; }
+ public bool IsVersioned() => Base.Major != AllVersions.Instance.Major;
///
/// Returns true if this versioning system represents a "versionless" product
/// (e.g., serverless, cloud services) that should not display a version dropdown.
diff --git a/src/Elastic.Documentation.Configuration/Versions/VersionInference.cs b/src/Elastic.Documentation.Configuration/Versions/VersionInference.cs
index 102dca32f..08259d8ae 100644
--- a/src/Elastic.Documentation.Configuration/Versions/VersionInference.cs
+++ b/src/Elastic.Documentation.Configuration/Versions/VersionInference.cs
@@ -96,7 +96,7 @@ public class NoopVersionInferrer : IVersionInferrerService
public VersioningSystem InferVersion(string repositoryName, IReadOnlyCollection? legacyPages, IReadOnlyCollection? products, ApplicableTo? applicableTo) => new()
{
Id = VersioningSystemId.Stack,
- Base = new SemVersion(0, 0, 0),
- Current = new SemVersion(0, 0, 0)
+ Base = ZeroVersion.Instance,
+ Current = ZeroVersion.Instance
};
}
diff --git a/src/Elastic.Documentation.Site/Assets/main.ts b/src/Elastic.Documentation.Site/Assets/main.ts
index 38f9fcfd6..0beab1d5e 100644
--- a/src/Elastic.Documentation.Site/Assets/main.ts
+++ b/src/Elastic.Documentation.Site/Assets/main.ts
@@ -2,7 +2,6 @@ import { initAppliesSwitch } from './applies-switch'
import { initCopyButton } from './copybutton'
import { initHighlight } from './hljs'
import { initImageCarousel } from './image-carousel'
-import './markdown/applies-to'
import { openDetailsWithAnchor } from './open-details-with-anchor'
import { initNav } from './pages-nav'
import { initSmoothScroll } from './smooth-scroll'
@@ -33,6 +32,7 @@ initializeOtel({
// Parcel will automatically code-split this into a separate chunk
import('./web-components/SearchOrAskAi/SearchOrAskAi')
import('./web-components/VersionDropdown')
+import('./web-components/AppliesToPopover')
const { getOS } = new UAParser()
const isLazyLoadNavigationEnabled =
diff --git a/src/Elastic.Documentation.Site/Assets/markdown/applies-to.css b/src/Elastic.Documentation.Site/Assets/markdown/applies-to.css
index d371819df..e09159ce8 100644
--- a/src/Elastic.Documentation.Site/Assets/markdown/applies-to.css
+++ b/src/Elastic.Documentation.Site/Assets/markdown/applies-to.css
@@ -4,14 +4,54 @@
@apply text-subdued;
- [data-tippy-content]:not([data-tippy-content='']) {
- @apply cursor-help;
+ applies-to-popover {
+ display: contents;
}
.applicable-info {
@apply border-grey-20 inline-flex cursor-default rounded-full border-[1px] bg-white pt-1.5 pr-3 pb-1.5 pl-3;
}
+ .applicable-info--clickable {
+ /* Desktop: tooltip-like behavior, no pointer cursor or hover state */
+ @apply cursor-default;
+
+ &:focus {
+ outline: none;
+ }
+
+ /* Desktop: no focus-visible outline for tooltip behavior */
+ @media (hover: hover) and (pointer: fine) {
+ &:focus-visible {
+ outline: none;
+ }
+ }
+
+ /* Mobile/touch: show focus outline for accessibility */
+ @media (hover: none), (pointer: coarse) {
+ @apply cursor-pointer;
+
+ &:focus-visible {
+ outline: 2px solid var(--color-blue-elastic);
+ outline-offset: 2px;
+ }
+ }
+ }
+
+ /* Desktop: no pinned state styling */
+ @media (hover: hover) and (pointer: fine) {
+ .applicable-info--pinned {
+ @apply border-grey-20 bg-white;
+ }
+ }
+
+ /* Mobile/touch: show pinned state */
+ @media (hover: none), (pointer: coarse) {
+ .applicable-info--pinned {
+ @apply border-blue-elastic bg-grey-10;
+ }
+ }
+
.applicable-meta {
@apply inline-flex gap-1.5;
}
@@ -35,6 +75,12 @@
.applies.applies-inline {
display: inline-block;
vertical-align: bottom;
+
+ applies-to-popover {
+ display: inline-flex;
+ vertical-align: bottom;
+ }
+
.applicable-separator {
margin-left: calc(var(--spacing) * 1.5);
margin-right: calc(var(--spacing) * 1.5);
@@ -57,19 +103,9 @@
}
}
-.tippy-box[data-theme~='applies-to'] {
- .tippy-content {
- white-space: normal;
-
- strong {
- display: block;
- margin-bottom: calc(var(--spacing) * 1);
- }
- }
-
- .tippy-content > div:not(:last-child) {
- border-bottom: 1px dotted var(--color-grey-50);
- padding-bottom: calc(var(--spacing) * 3);
- margin-bottom: calc(var(--spacing) * 3);
- }
+.euiPopover__panel {
+ /* Shadow and border for the popover */
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1) !important;
+ border: 1px solid var(--color-grey-20) !important;
+ border-radius: 6px !important;
}
diff --git a/src/Elastic.Documentation.Site/Assets/markdown/applies-to.ts b/src/Elastic.Documentation.Site/Assets/markdown/applies-to.ts
deleted file mode 100644
index df5ad9d66..000000000
--- a/src/Elastic.Documentation.Site/Assets/markdown/applies-to.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { $$ } from 'select-dom'
-import tippy from 'tippy.js'
-
-document.addEventListener('htmx:load', function () {
- const selector = [
- '.applies [data-tippy-content]:not([data-tippy-content=""])',
- '.applies-inline [data-tippy-content]:not([data-tippy-content=""])',
- ].join(', ')
-
- const appliesToBadgesWithTooltip = $$(selector)
- appliesToBadgesWithTooltip.forEach((badge) => {
- const content = badge.getAttribute('data-tippy-content')
- if (!content) return
- tippy(badge, {
- content,
- allowHTML: true,
- delay: [400, 100],
- hideOnClick: false,
- ignoreAttributes: true,
- theme: 'applies-to',
- })
- })
-})
diff --git a/src/Elastic.Documentation.Site/Assets/web-components/AppliesToPopover.tsx b/src/Elastic.Documentation.Site/Assets/web-components/AppliesToPopover.tsx
new file mode 100644
index 000000000..b324b39d2
--- /dev/null
+++ b/src/Elastic.Documentation.Site/Assets/web-components/AppliesToPopover.tsx
@@ -0,0 +1,515 @@
+'use strict'
+
+import '../eui-icons-cache'
+import { EuiPopover, useGeneratedHtmlId } from '@elastic/eui'
+import { css } from '@emotion/react'
+import r2wc from '@r2wc/react-to-web-component'
+import * as React from 'react'
+import { useState, useRef, useEffect, useCallback } from 'react'
+
+type PopoverAvailabilityItem = {
+ text: string
+ lifecycleDescription?: string
+}
+
+type PopoverData = {
+ productDescription?: string
+ availabilityItems: PopoverAvailabilityItem[]
+ additionalInfo?: string
+ showVersionNote: boolean
+ versionNote?: string
+}
+
+type AppliesToPopoverProps = {
+ badgeKey?: string
+ badgeLifecycleText?: string
+ badgeVersion?: string
+ lifecycleClass?: string
+ lifecycleName?: string
+ showLifecycleName?: boolean
+ showVersion?: boolean
+ hasMultipleLifecycles?: boolean
+ popoverData?: PopoverData
+ showPopover?: boolean
+ isInline?: boolean
+}
+
+const AppliesToPopover = ({
+ badgeKey,
+ badgeLifecycleText,
+ badgeVersion,
+ lifecycleClass,
+ lifecycleName,
+ showLifecycleName,
+ showVersion,
+ hasMultipleLifecycles,
+ popoverData,
+ showPopover = true,
+ isInline = false,
+}: AppliesToPopoverProps) => {
+ const [isOpen, setIsOpen] = useState(false)
+ const [isPinned, setIsPinned] = useState(false)
+ const [openItems, setOpenItems] = useState>(new Set())
+ const [isTouchDevice, setIsTouchDevice] = useState(false)
+ const popoverId = useGeneratedHtmlId({ prefix: 'appliesToPopover' })
+ const contentRef = useRef(null)
+ const badgeRef = useRef(null)
+ const hoverTimeoutRef = useRef | null>(null)
+
+ // Detect touch device on mount
+ useEffect(() => {
+ const checkTouchDevice = () => {
+ const hasCoarsePointer =
+ window.matchMedia('(pointer: coarse)').matches
+ const hasNoHover = window.matchMedia('(hover: none)').matches
+ setIsTouchDevice(hasCoarsePointer || hasNoHover)
+ }
+ checkTouchDevice()
+ // Re-check on resize in case device mode changes (e.g., dev tools toggle)
+ window.addEventListener('resize', checkTouchDevice)
+ return () => window.removeEventListener('resize', checkTouchDevice)
+ }, [])
+
+ const hasPopoverContent =
+ popoverData &&
+ (popoverData.productDescription ||
+ popoverData.availabilityItems.length > 0 ||
+ popoverData.additionalInfo ||
+ popoverData.showVersionNote)
+
+ const openPopover = useCallback(() => {
+ if (showPopover && hasPopoverContent) {
+ setIsOpen(true)
+ }
+ }, [showPopover, hasPopoverContent])
+
+ const closePopover = useCallback(() => {
+ if (!isPinned) {
+ setIsOpen(false)
+ }
+ }, [isPinned])
+
+ const handleClick = useCallback(() => {
+ // Only allow click/pin behavior on touch devices
+ // On desktop, the popover is tooltip-like (hover only)
+ if (!isTouchDevice) return
+
+ if (showPopover && hasPopoverContent) {
+ if (isPinned) {
+ // If already pinned, unpin and close
+ setIsPinned(false)
+ setIsOpen(false)
+ } else {
+ // Pin the popover open
+ setIsPinned(true)
+ setIsOpen(true)
+ }
+ }
+ }, [showPopover, hasPopoverContent, isPinned, isTouchDevice])
+
+ const toggleItem = useCallback((index: number, e: React.MouseEvent) => {
+ e.stopPropagation()
+ setOpenItems((prev) => {
+ const next = new Set(prev)
+ if (next.has(index)) {
+ next.delete(index)
+ } else {
+ next.add(index)
+ }
+ return next
+ })
+ }, [])
+
+ const handleClosePopover = useCallback(() => {
+ setIsPinned(false)
+ setIsOpen(false)
+ }, [])
+
+ const handleMouseEnter = useCallback(() => {
+ // Clear any pending timeout (open or close)
+ if (hoverTimeoutRef.current) {
+ clearTimeout(hoverTimeoutRef.current)
+ hoverTimeoutRef.current = null
+ }
+ // Delay opening to prevent accidental triggers while scanning
+ // Matches EUI tooltip default delay (~250ms)
+ hoverTimeoutRef.current = setTimeout(() => {
+ openPopover()
+ }, 250)
+ }, [openPopover])
+
+ const handleMouseLeave = useCallback(() => {
+ // Clear any pending open timeout
+ if (hoverTimeoutRef.current) {
+ clearTimeout(hoverTimeoutRef.current)
+ hoverTimeoutRef.current = null
+ }
+ // Small delay before closing to allow moving to the popover content
+ hoverTimeoutRef.current = setTimeout(() => {
+ closePopover()
+ }, 150)
+ }, [closePopover])
+
+ // Cleanup timeout on unmount
+ useEffect(() => {
+ return () => {
+ if (hoverTimeoutRef.current) {
+ clearTimeout(hoverTimeoutRef.current)
+ }
+ }
+ }, [])
+
+ // Close popover when badge becomes hidden (e.g., parent details element collapses)
+ useEffect(() => {
+ if (!badgeRef.current || !isOpen) return
+
+ const observer = new IntersectionObserver(
+ (entries) => {
+ entries.forEach((entry) => {
+ // If badge is no longer visible, close the popover
+ if (!entry.isIntersecting) {
+ setIsPinned(false)
+ setIsOpen(false)
+ }
+ })
+ },
+ { threshold: 0 }
+ )
+
+ observer.observe(badgeRef.current)
+
+ return () => {
+ observer.disconnect()
+ }
+ }, [isOpen])
+
+ // Reset open items when popover closes
+ useEffect(() => {
+ if (!isOpen) {
+ setOpenItems(new Set())
+ }
+ }, [isOpen])
+
+ const showSeparator =
+ badgeKey && (showLifecycleName || showVersion || badgeLifecycleText)
+
+ // Only show interactive attributes on touch devices
+ const isInteractive = showPopover && hasPopoverContent && isTouchDevice
+
+ const badgeButton = (
+ {
+ if (isInteractive && (e.key === 'Enter' || e.key === ' ')) {
+ e.preventDefault()
+ handleClick()
+ }
+ }}
+ >
+ {badgeKey}
+
+ {showSeparator && }
+
+
+ {showLifecycleName && (
+
+ {lifecycleName}
+
+ )}
+ {showVersion ? (
+
+ {badgeVersion}
+
+ ) : (
+ badgeLifecycleText
+ )}
+ {hasMultipleLifecycles && (
+
+
+
+
+
+ )}
+
+
+ )
+
+ if (!showPopover || !hasPopoverContent) {
+ return badgeButton
+ }
+
+ const renderAvailabilityItem = (
+ item: PopoverAvailabilityItem,
+ index: number
+ ) => {
+ const isItemOpen = openItems.has(index)
+
+ if (item.lifecycleDescription) {
+ return (
+
+
+ )
+}
+
+customElements.define(
+ 'applies-to-popover',
+ r2wc(AppliesToPopover, {
+ props: {
+ badgeKey: 'string',
+ badgeLifecycleText: 'string',
+ badgeVersion: 'string',
+ lifecycleClass: 'string',
+ lifecycleName: 'string',
+ showLifecycleName: 'boolean',
+ showVersion: 'boolean',
+ hasMultipleLifecycles: 'boolean',
+ popoverData: 'json',
+ showPopover: 'boolean',
+ isInline: 'boolean',
+ },
+ })
+)
+
+export default AppliesToPopover
diff --git a/src/Elastic.Documentation/AppliesTo/Applicability.cs b/src/Elastic.Documentation/AppliesTo/Applicability.cs
index b91fdb991..2164a485e 100644
--- a/src/Elastic.Documentation/AppliesTo/Applicability.cs
+++ b/src/Elastic.Documentation/AppliesTo/Applicability.cs
@@ -37,13 +37,88 @@ public static bool TryParse(string? value, IList<(Severity, string)> diagnostics
if (applications.Count == 0)
return false;
+ // Infer version semantics when multiple items have GreaterThanOrEqual versions
+ applications = InferVersionSemantics(applications);
+
// Sort by version in descending order (the highest version first)
- // Items without versions (AllVersions.Instance) are sorted last
+ // Items without versions (AllVersionsSpec.Instance) are sorted last
var sortedApplications = applications.OrderDescending().ToArray();
availability = new AppliesCollection(sortedApplications);
return true;
}
+ ///
+ /// Infers versioning semantics according to the following ruleset:
+ /// - The highest version keeps GreaterThanOrEqual (e.g., 9.4+)
+ /// - Lower versions become Exact if consecutive, or Range to fill gaps
+ /// - This rule only applies when all versions are at minor level (patch = 0).
+ ///
+ private static List InferVersionSemantics(List applications)
+ {
+ // Get items with actual GreaterThanOrEqual versions (not AllVersionsSpec, not null, not ranges/exact)
+ var gteItems = applications
+ .Where(a => a.Version is { Kind: VersionSpecKind.GreaterThanOrEqual }
+ && a.Version != AllVersionsSpec.Instance)
+ .ToList();
+
+ // If 0 or 1 GTE items, no inference needed
+ if (gteItems.Count <= 1)
+ return applications;
+
+ // Only apply inference when all entries are on patch version 0
+ if (gteItems.Any(a => a.Version!.Min.Patch != 0))
+ return applications;
+
+ // Sort GTE items by version ascending to process from lowest to highest
+ var sortedGteVersions = gteItems
+ .Select(a => a.Version!.Min)
+ .Distinct()
+ .OrderBy(v => v)
+ .ToList();
+
+ if (sortedGteVersions.Count <= 1)
+ return applications;
+
+ var versionMapping = new Dictionary();
+
+ for (var i = 0; i < sortedGteVersions.Count; i++)
+ {
+ var currentVersion = sortedGteVersions[i];
+
+ if (i == sortedGteVersions.Count - 1)
+ {
+ // Highest version keeps GreaterThanOrEqual
+ versionMapping[currentVersion] = VersionSpec.GreaterThanOrEqual(currentVersion);
+ }
+ else
+ {
+ var nextVersion = sortedGteVersions[i + 1];
+
+ // Define an Exact or Range VersionSpec according to the numeric difference between lifecycles
+ if (currentVersion.Major == nextVersion.Major
+ && nextVersion.Minor == currentVersion.Minor + 1)
+ versionMapping[currentVersion] = VersionSpec.Exact(currentVersion);
+ else
+ {
+ var rangeEnd = new SemVersion(nextVersion.Major, nextVersion.Minor == 0 ? nextVersion.Minor : nextVersion.Minor - 1, 0);
+ versionMapping[currentVersion] = VersionSpec.Range(currentVersion, rangeEnd);
+ }
+ }
+ }
+
+ // Apply the mapping to create updated applications
+ return applications.Select(a =>
+ {
+ if (a.Version is null or AllVersionsSpec || a is not { Version.Kind: VersionSpecKind.GreaterThanOrEqual })
+ return a;
+
+ if (versionMapping.TryGetValue(a.Version.Min, out var newSpec))
+ return a with { Version = newSpec };
+
+ return a;
+ }).ToList();
+ }
+
public virtual bool Equals(AppliesCollection? other)
{
if ((object)this == other)
@@ -98,36 +173,24 @@ public override string ToString()
public record Applicability : IComparable, IComparable
{
public ProductLifecycle Lifecycle { get; init; }
- public SemVersion? Version { get; init; }
+ public VersionSpec? Version { get; init; }
public static Applicability GenerallyAvailable { get; } = new()
{
Lifecycle = ProductLifecycle.GenerallyAvailable,
- Version = AllVersions.Instance
+ Version = AllVersionsSpec.Instance
};
public string GetLifeCycleName() =>
- Lifecycle switch
- {
- ProductLifecycle.TechnicalPreview => "Preview",
- ProductLifecycle.Beta => "Beta",
- ProductLifecycle.Development => "Development",
- ProductLifecycle.Deprecated => "Deprecated",
- ProductLifecycle.Planned => "Planned",
- ProductLifecycle.Discontinued => "Discontinued",
- ProductLifecycle.Unavailable => "Unavailable",
- ProductLifecycle.GenerallyAvailable => "GA",
- ProductLifecycle.Removed => "Removed",
- _ => throw new ArgumentOutOfRangeException(nameof(Lifecycle), Lifecycle, null)
- };
+ ProductLifecycleInfo.GetShortName(Lifecycle);
///
public int CompareTo(Applicability? other)
{
- var xIsNonVersioned = Version is null || ReferenceEquals(Version, AllVersions.Instance);
- var yIsNonVersioned = other?.Version is null || ReferenceEquals(other.Version, AllVersions.Instance);
+ var xIsNonVersioned = Version is null || ReferenceEquals(Version, AllVersionsSpec.Instance);
+ var yIsNonVersioned = other?.Version is null || ReferenceEquals(other.Version, AllVersionsSpec.Instance);
if (xIsNonVersioned && yIsNonVersioned)
return 0;
@@ -158,7 +221,7 @@ public override string ToString()
_ => throw new ArgumentOutOfRangeException()
};
_ = sb.Append(lifecycle);
- if (Version is not null && Version != AllVersions.Instance)
+ if (Version is not null && Version != AllVersionsSpec.Instance)
_ = sb.Append(' ').Append(Version);
return sb.ToString();
}
@@ -224,10 +287,10 @@ public static bool TryParse(string? value, IList<(Severity, string)> diagnostics
? null
: tokens[1] switch
{
- null => AllVersions.Instance,
- "all" => AllVersions.Instance,
- "" => AllVersions.Instance,
- var t => SemVersionConverter.TryParse(t, out var v) ? v : null
+ null => AllVersionsSpec.Instance,
+ "all" => AllVersionsSpec.Instance,
+ "" => AllVersionsSpec.Instance,
+ var t => VersionSpec.TryParse(t, out var v) ? v : null
};
availability = new Applicability { Version = version, Lifecycle = lifecycle };
return true;
diff --git a/src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs b/src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs
index cb881fbf6..eb4a0f7f3 100644
--- a/src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs
+++ b/src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs
@@ -15,44 +15,30 @@ public static class ApplicabilitySelector
/// The collection of applicabilities to select from
/// The current version to use for comparison
/// The most relevant applicability for display
- public static Applicability GetPrimaryApplicability(IEnumerable applicabilities, SemVersion currentVersion)
+ public static Applicability GetPrimaryApplicability(IReadOnlyCollection applicabilities, SemVersion currentVersion)
{
- var applicabilityList = applicabilities.ToList();
- var lifecycleOrder = new Dictionary
- {
- [ProductLifecycle.GenerallyAvailable] = 0,
- [ProductLifecycle.Beta] = 1,
- [ProductLifecycle.TechnicalPreview] = 2,
- [ProductLifecycle.Planned] = 3,
- [ProductLifecycle.Deprecated] = 4,
- [ProductLifecycle.Removed] = 5,
- [ProductLifecycle.Unavailable] = 6
- };
-
- var availableApplicabilities = applicabilityList
- .Where(a => a.Version is null || a.Version is AllVersions || a.Version <= currentVersion)
- .ToList();
+ var availableApplicabilities = applicabilities
+ .Where(a => a.Version is null || a.Version is AllVersionsSpec || a.Version.Min <= currentVersion).ToArray();
- if (availableApplicabilities.Count != 0)
+ if (availableApplicabilities.Length > 0)
{
return availableApplicabilities
- .OrderByDescending(a => a.Version ?? new SemVersion(0, 0, 0))
- .ThenBy(a => lifecycleOrder.GetValueOrDefault(a.Lifecycle, 999))
+ .OrderByDescending(a => a.Version?.Min ?? ZeroVersion.Instance)
+ .ThenBy(a => ProductLifecycleInfo.GetOrder(a.Lifecycle))
.First();
}
- var futureApplicabilities = applicabilityList
- .Where(a => a.Version is not null && a.Version is not AllVersions && a.Version > currentVersion)
- .ToList();
+ var futureApplicabilities = applicabilities
+ .Where(a => a.Version is not null && a.Version is not AllVersionsSpec && a.Version.Min > currentVersion).ToArray();
- if (futureApplicabilities.Count != 0)
+ if (futureApplicabilities.Length > 0)
{
return futureApplicabilities
- .OrderBy(a => a.Version!.CompareTo(currentVersion))
- .ThenBy(a => lifecycleOrder.GetValueOrDefault(a.Lifecycle, 999))
+ .OrderBy(a => a.Version!.Min.CompareTo(currentVersion))
+ .ThenBy(a => ProductLifecycleInfo.GetOrder(a.Lifecycle))
.First();
}
- return applicabilityList.First();
+ return applicabilities.First();
}
}
diff --git a/src/Elastic.Documentation/AppliesTo/ApplicableTo.cs b/src/Elastic.Documentation/AppliesTo/ApplicableTo.cs
index 6bfa16b4d..8914890a9 100644
--- a/src/Elastic.Documentation/AppliesTo/ApplicableTo.cs
+++ b/src/Elastic.Documentation/AppliesTo/ApplicableTo.cs
@@ -64,9 +64,11 @@ public record ApplicableTo
Product = AppliesCollection.GenerallyAvailable
};
+ private static readonly VersionSpec DefaultVersion = VersionSpec.TryParse("9.0", out var v) ? v! : AllVersionsSpec.Instance;
+
public static ApplicableTo Default { get; } = new()
{
- Stack = new AppliesCollection([new Applicability { Version = new SemVersion(9, 0, 0), Lifecycle = ProductLifecycle.GenerallyAvailable }]),
+ Stack = new AppliesCollection([new Applicability { Version = DefaultVersion, Lifecycle = ProductLifecycle.GenerallyAvailable }]),
Serverless = ServerlessProjectApplicability.All
};
diff --git a/src/Elastic.Documentation/AppliesTo/ApplicableToJsonConverter.cs b/src/Elastic.Documentation/AppliesTo/ApplicableToJsonConverter.cs
index d3779e525..c8d987064 100644
--- a/src/Elastic.Documentation/AppliesTo/ApplicableToJsonConverter.cs
+++ b/src/Elastic.Documentation/AppliesTo/ApplicableToJsonConverter.cs
@@ -44,7 +44,7 @@ public class ApplicableToJsonConverter : JsonConverter
string? type = null;
string? subType = null;
var lifecycle = ProductLifecycle.GenerallyAvailable;
- SemVersion? version = null;
+ VersionSpec? version = null;
while (reader.Read())
{
@@ -72,8 +72,14 @@ public class ApplicableToJsonConverter : JsonConverter
break;
case "version":
var versionStr = reader.GetString();
- if (versionStr != null && SemVersionConverter.TryParse(versionStr, out var v))
- version = v;
+ if (versionStr != null)
+ {
+ // Handle "all" explicitly for AllVersionsSpec
+ if (string.Equals(versionStr.Trim(), "all", StringComparison.OrdinalIgnoreCase))
+ version = AllVersionsSpec.Instance;
+ else if (VersionSpec.TryParse(versionStr, out var v))
+ version = v;
+ }
break;
}
}
diff --git a/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs b/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs
index 1017207e1..b22c59ed4 100644
--- a/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs
+++ b/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs
@@ -256,10 +256,109 @@ private static bool TryGetApplicabilityOverTime(Dictionary
+"""
+
+type ``stack and ece future versions`` () =
static let markdown = Setup.Markdown """
```{applies_to}
stack: beta 9.1.0
@@ -627,211 +427,317 @@ deployment:
"""
[]
- let ``renders missing lifecycle scenarios`` () =
+ let ``renders stack and ece planned`` () =
markdown |> convertsToHtml """
-
+
+
+
+
+"""
+
+type ``stack empty defaults to ga`` () =
+ static let markdown = Setup.Markdown """
+```{applies_to}
+stack:
+```
+"""
-Beta features are subject to change. The design and code is less mature than official GA features and is being provided as-is with no warranties. Beta features are not subject to the support SLA of official GA features.">
- Stack
-
-
- Planned
-
-
-
- ECE
-
-
- Planned
-
-
+ []
+ let ``no version defaults to ga`` () =
+ markdown |> convertsToHtml """
+
+
+
"""
-// Test missing version scenarios
-type ``version scenarios missing`` () =
+// Test missing VersioningSystemId coverage
+type ``all products future version coverage`` () =
static let markdown = Setup.Markdown """
```{applies_to}
-stack: beta 9.1.0
+stack: ga 9.0.0
+serverless: ga 9.0.0
deployment:
- ece: ga 9.1.0
+ ece: ga 9.0.0
+ eck: ga 9.0.0
+ ess: ga 9.0.0
+ self: ga 9.0.0
+product: ga 9.0.0
```
"""
[]
- let ``renders missing version scenarios`` () =
+ let ``renders VersioningSystemId coverage`` () =
markdown |> convertsToHtml """
-
+
+
+
+
+
+
+
+"""
-Beta features are subject to change. The design and code is less mature than official GA features and is being provided as-is with no warranties. Beta features are not subject to the support SLA of official GA features.">
- Stack
-
-
- Planned
-
-
-
- ECE
-
-
- Planned
-
-
+// Test multiple lifecycles for same applicability key
+// With version inference: ga 8.0, beta 8.1 → ga =8.0 (exact), beta 8.1+ (highest gets GTE)
+type ``ga with beta uses version inference`` () =
+ static let markdown = Setup.Markdown """
+```{applies_to}
+stack: ga 8.0.0, beta 8.1.0
+```
+"""
+
+ []
+ let ``renders multiple lifecycles with ellipsis and shows GA lifecycle`` () =
+ markdown |> convertsToHtml """
+
+
+
"""
-// Test missing edge cases
-type ``edge cases missing`` () =
+type ``stack ga released version`` () =
static let markdown = Setup.Markdown """
```{applies_to}
-stack:
+stack: ga 7.0.0
```
"""
[]
- let ``renders missing edge cases`` () =
+ let ``renders ga since released version`` () =
markdown |> convertsToHtml """
-
+
+
+"""
+
+type ``stack preview released version`` () =
+ static let markdown = Setup.Markdown """
+```{applies_to}
+stack: preview 7.0.0
+```
+"""
-If this functionality is unavailable or behaves differently when deployed on ECH, ECE, ECK, or a self-managed installation, it will be indicated on the page.">
- Stack
-
-
-
+ []
+ let ``renders preview since released version`` () =
+ markdown |> convertsToHtml """
+
+
+
"""
-// Test missing VersioningSystemId coverage
-type ``versioning system id coverage`` () =
+type ``stack beta released version`` () =
static let markdown = Setup.Markdown """
```{applies_to}
-stack: ga 9.0.0
-serverless: ga 9.0.0
-deployment:
- ece: ga 9.0.0
- eck: ga 9.0.0
- ess: ga 9.0.0
- self: ga 9.0.0
-product: ga 9.0.0
+stack: beta 7.0.0
```
"""
[]
- let ``renders missing VersioningSystemId coverage`` () =
- markdown |> convertsToHtml """
-
-"""
-
-// Test missing disclaimer scenarios
-type ``disclaimer scenarios`` () =
+ let ``renders beta since released version`` () =
+ markdown |> convertsToHtml """
+
+
+
+
+"""
+
+type ``stack deprecated released version`` () =
static let markdown = Setup.Markdown """
```{applies_to}
-stack: ga 9.0.0
+stack: deprecated 7.0.0
```
"""
[]
- let ``renders missing disclaimer scenarios`` () =
+ let ``renders deprecated since released version`` () =
markdown |> convertsToHtml """
-
+
+
+"""
-If this functionality is unavailable or behaves differently when deployed on ECH, ECE, ECK, or a self-managed installation, it will be indicated on the page.">
- Stack
-
-
- Planned
-
-
+type ``stack removed released version`` () =
+ static let markdown = Setup.Markdown """
+```{applies_to}
+stack: removed 7.0.0
+```
+"""
+
+ []
+ let ``renders removed in released version`` () =
+ markdown |> convertsToHtml """
+
+
+
"""
-// Test multiple lifecycles for same applicability key
-type ``multiple lifecycles same key`` () =
+// Version spec syntax tests (exact and range)
+type ``stack ga exact version released`` () =
static let markdown = Setup.Markdown """
```{applies_to}
-stack: ga 8.0.0, beta 8.1.0
+stack: ga =7.5
```
"""
[]
- let ``renders multiple lifecycles with ellipsis and shows GA lifecycle`` () =
+ let ``renders ga in exact released version`` () =
markdown |> convertsToHtml """
-
+
+
+"""
-If this functionality is unavailable or behaves differently when deployed on ECH, ECE, ECK, or a self-managed installation, it will be indicated on the page.
+type ``stack ga range both released`` () =
+ static let markdown = Setup.Markdown """
+```{applies_to}
+stack: ga 7.0-8.0
+```
+"""
+
+ []
+ let ``renders ga from-to when both ends released`` () =
+ markdown |> convertsToHtml """
+
+
+
+
+"""
+
+type ``stack ga range max unreleased`` () =
+ static let markdown = Setup.Markdown """
+```{applies_to}
+stack: ga 7.0-9.0
+```
+"""
+
+ []
+ let ``renders ga since min when max unreleased`` () =
+ markdown |> convertsToHtml """
+
+
+
+
+"""
+
+// Multiple released lifecycles showing both in popover
+type ``preview and ga both released`` () =
+ static let markdown = Setup.Markdown """
+```{applies_to}
+stack: preview 7.0, ga 7.5
+```
+"""
+
+ []
+ let ``renders ga badge with both lifecycles in popover`` () =
+ markdown |> convertsToHtml """
+
+
+
+
+"""
+
+type ``explicit patch version with exclamation mark`` () =
+ static let markdown = Setup.Markdown """
+```{applies_to}
+stack: preview 7.5.4!
+```
+"""
+
+ []
+ let ``renders patch version when explicitly requested with exclamation mark`` () =
+ markdown |> convertsToHtml """
+
+
+
+
+"""
+
+type ``patch version hidden without exclamation mark`` () =
+ static let markdown = Setup.Markdown """
+```{applies_to}
+stack: preview 7.5.4
+```
+"""
+
+ []
+ let ``hides patch version when no exclamation mark used`` () =
+ markdown |> convertsToHtml """
+
+
+
+
+"""
+
+type ``range with explicit patch on both ends`` () =
+ static let markdown = Setup.Markdown """
+```{applies_to}
+stack: beta 7.0.3!-7.5.2!
+```
+"""
-
Elastic Stack Beta 8.1.0:We plan to add this functionality in a future Elastic Stack update. Subject to change.
+ []
+ let ``renders range with patch versions when both have exclamation marks`` () =
+ markdown |> convertsToHtml """
+
+
+
+
+"""
+
+type ``range with explicit patch only on mininum version`` () =
+ static let markdown = Setup.Markdown """
+```{applies_to}
+stack: ga 7.0.5!-7.5
+```
+"""
-Beta features are subject to change. The design and code is less mature than official GA features and is being provided as-is with no warranties. Beta features are not subject to the support SLA of official GA features.
">
- Stack
-
-
- GA
-
- 8.0.0
-
-
-
-
-
-
-
-
+ []
+ let ``renders range with patch on mininum version only when explicit operator is used`` () =
+ markdown |> convertsToHtml """
+
+
+
+
+"""
+
+type ``range with explicit patch only on maximum version`` () =
+ static let markdown = Setup.Markdown """
+```{applies_to}
+stack: ga 7.0-7.5.3!
+```
+"""
+
+ []
+ let ``renders range with patch on maxinum version only when explicit operator is used`` () =
+ markdown |> convertsToHtml """
+
+
+
+
+"""
+
+type ``exact version with explicit patch`` () =
+ static let markdown = Setup.Markdown """
+```{applies_to}
+stack: ga =7.5.3!
+```
+"""
+
+ []
+ let ``renders exact version with patch when explicit operator is used`` () =
+ markdown |> convertsToHtml """
+
+
+
"""
diff --git a/tests/authoring/Applicability/AppliesToFrontMatter.fs b/tests/authoring/Applicability/AppliesToFrontMatter.fs
index 2d1f02b95..95483372e 100644
--- a/tests/authoring/Applicability/AppliesToFrontMatter.fs
+++ b/tests/authoring/Applicability/AppliesToFrontMatter.fs
@@ -163,10 +163,7 @@ applies_to:
[]
let ``apply matches expected`` () =
markdown |> appliesTo (ApplicableTo(
- Product=AppliesCollection([
- Applicability.op_Explicit "removed 9.7";
- Applicability.op_Explicit "preview 9.5"
- ] |> Array.ofList)
+ Product=AppliesCollection.op_Explicit "removed 9.7, preview 9.5"
))
type ``lenient to defining types at top level`` () =
diff --git a/tests/authoring/Blocks/Admonitions.fs b/tests/authoring/Blocks/Admonitions.fs
index d7efdb64b..e890b14fa 100644
--- a/tests/authoring/Blocks/Admonitions.fs
+++ b/tests/authoring/Blocks/Admonitions.fs
@@ -64,13 +64,7 @@ This is a custom admonition with applies_to information.
Note
-
- Stack
-
-
-
+
@@ -82,11 +76,7 @@ If this functionality is unavailable or behaves differently when deployed on ECH
Warning
-
- Serverless
-
-
-
+
@@ -98,15 +88,7 @@ If this functionality is unavailable or behaves differently when deployed on ECH