Skip to content

Commit 241eac9

Browse files
miharpclaude
andcommitted
Add a per-product documentation version selector
Render a version picker at the top of the sidebar, driven by _data/products.yml. For the current product it lists the versions (newest first), marks the one being viewed, and badges the version that `latest` aliases. Version links point at each version's root; switching is an infrequent, deliberate jump, and root links stay correct in the theme's persistent (Turbo-navigated) sidebar without per-page rewriting (keeping the reader on the same page across a switch is a possible later enhancement). - _includes/version-selector.html renders a block per qualifying product (2+ versions, not single_version) so the picker survives Turbo navigation between products. The wrapper is only emitted when at least one product qualifies, so nothing shows while every product still has a single version. - layout_end.html gains syncVersionPicker, which on each Turbo frame load shows the block matching the current collection and marks the active version (with a fallback to the `latest`-aliased version on /<product>/latest/ pages), mirroring the existing nav-group sync. - custom.css styles the picker to match the theme. Verified against a local two-version test bed (OpenVox 8.x + a 9.0.0-alpha1 9.x): correct active marking and latest badge on direct loads of 8.x, 9.x and the latest alias; correct group swap and active marking across Turbo navigation between products; OpenVox Containers (single_version) excluded; and no picker rendered while all products have one version. Part of #325. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Michael Harp <mike@mikeharp.com>
1 parent c5e4334 commit 241eac9

4 files changed

Lines changed: 164 additions & 0 deletions

File tree

_includes/jekyll_vitepress/layout_end.html

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,38 @@
99
group.hidden = collections.indexOf(currentCollection) < 0;
1010
});
1111
}
12+
// Show the version picker for the current product and mark the active version.
13+
// The sidebar persists across Turbo navigation, so this re-runs per frame load.
14+
function syncVersionPicker() {
15+
var frameState = document.getElementById('vp-page-state');
16+
if (!frameState) return;
17+
var currentCollection = frameState.getAttribute('data-collection') || '';
18+
document.querySelectorAll('.VPVersionPickerGroup').forEach(function (group) {
19+
var collections = (group.getAttribute('data-collections') || '').split('|');
20+
var isCurrent = collections.indexOf(currentCollection) >= 0;
21+
group.hidden = !isCurrent;
22+
if (!isCurrent) return;
23+
// The _latest alias collection has no version link of its own; fall back
24+
// to the version `latest` points at.
25+
var target = currentCollection;
26+
if (!group.querySelector('.VPVersionPickerItem[data-collection="' + currentCollection + '"]')) {
27+
target = group.getAttribute('data-latest-collection') || '';
28+
}
29+
group.querySelectorAll('.VPVersionPickerItem').forEach(function (link) {
30+
var active = link.getAttribute('data-collection') === target;
31+
link.classList.toggle('is-active', active);
32+
if (active) {
33+
link.setAttribute('aria-current', 'true');
34+
} else {
35+
link.removeAttribute('aria-current');
36+
}
37+
});
38+
});
39+
}
1240
document.addEventListener('turbo:frame-load', function (event) {
1341
if (!event.target || event.target.id !== 'vp-content-frame') return;
1442
syncSidebarNavGroups();
43+
syncVersionPicker();
1544
});
1645
})();
1746

_includes/sidebar.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
<nav class="nav" id="VPSidebarNav" aria-labelledby="sidebar-aria-label" tabindex="-1">
1212
<span class="visually-hidden" id="sidebar-aria-label">Sidebar Navigation</span>
1313

14+
{% include version-selector.html %}
15+
1416
{%- assign current_nav_collection_dir = page.collection | prepend: '_' | append: '/' -%}
1517
{%- assign current_nav_page_subpath = page.relative_path | remove_first: current_nav_collection_dir | replace: '.md', '.html' | replace: '.markdown', '.html' -%}
1618
{%- assign current_nav_base = page.url | remove: current_nav_page_subpath -%}

_includes/version-selector.html

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
{%- comment -%}
2+
Per-product documentation version selector, rendered at the top of the sidebar.
3+
4+
Lists the current product's versions (newest first, per _data/products.yml),
5+
marks the one being viewed, and badges the version that `latest` aliases. Links
6+
point at each version's root (e.g. /openvox/9.x/); switching versions is a
7+
deliberate, infrequent jump, and root links stay correct in the theme's
8+
persistent (Turbo-navigated) sidebar without per-page rewriting. Keeping the
9+
reader on the same page across a switch is a possible later enhancement.
10+
11+
Each qualifying product gets a block so the selector survives Turbo navigation
12+
between products; the layout-end script shows the block matching the current
13+
collection and marks the active version. A product is skipped when it is flagged
14+
single_version (no numbered collections, e.g. OpenVox Containers) or has fewer
15+
than two versions — there is nothing to switch between, so no picker is shown.
16+
The wrapper is only emitted when at least one product qualifies, to avoid a
17+
stray empty bordered box.
18+
{%- endcomment -%}
19+
{%- capture version_picker_groups -%}
20+
{%- for product_entry in site.data.products -%}
21+
{%- assign product_id = product_entry[0] -%}
22+
{%- assign product = product_entry[1] -%}
23+
{%- if product.single_version == true -%}{%- continue -%}{%- endif -%}
24+
{%- if product.versions.size < 2 -%}{%- continue -%}{%- endif -%}
25+
26+
{%- comment -%} Collections this product owns: each numbered version, plus the _latest alias. {%- endcomment -%}
27+
{%- assign latest_alias = product_id | append: '_latest' -%}
28+
{%- assign collections = '' -%}
29+
{%- assign latest_collection = '' -%}
30+
{%- for version in product.versions -%}
31+
{%- assign version_collection = version.collection | remove_first: '_' -%}
32+
{%- assign collections = collections | append: version_collection | append: '|' -%}
33+
{%- if version.id == product.latest -%}{%- assign latest_collection = version_collection -%}{%- endif -%}
34+
{%- endfor -%}
35+
{%- assign collections = collections | append: latest_alias -%}
36+
{%- assign collection_list = collections | split: '|' -%}
37+
38+
{%- assign is_current_product = false -%}
39+
{%- if collection_list contains page.collection -%}{%- assign is_current_product = true -%}{%- endif -%}
40+
41+
<div class="VPVersionPickerGroup" data-product="{{ product_id }}" data-collections="{{ collections }}" data-latest-collection="{{ latest_collection }}"{% unless is_current_product %} hidden{% endunless %}>
42+
<span class="VPVersionPickerLabel">{{ product.label }} version</span>
43+
<ul class="VPVersionPickerList">
44+
{%- for version in product.versions -%}
45+
{%- assign version_collection = version.collection | remove_first: '_' -%}
46+
{%- assign is_active = false -%}
47+
{%- if version_collection == page.collection -%}
48+
{%- assign is_active = true -%}
49+
{%- elsif page.collection == latest_alias and version.id == product.latest -%}
50+
{%- assign is_active = true -%}
51+
{%- endif -%}
52+
<li>
53+
<a class="VPVersionPickerItem{% if is_active %} is-active{% endif %}" href="{{ version.base }}" data-collection="{{ version_collection }}"{% if is_active %} aria-current="true"{% endif %}>
54+
<span class="VPVersionPickerVersion">{{ version.label }}</span>
55+
{%- if version.id == product.latest -%}<span class="VPVersionPickerBadge">latest</span>{%- endif -%}
56+
</a>
57+
</li>
58+
{%- endfor -%}
59+
</ul>
60+
</div>
61+
{%- endfor -%}
62+
{%- endcapture -%}
63+
{%- assign version_picker_groups = version_picker_groups | strip -%}
64+
{%- if version_picker_groups != '' -%}
65+
<div class="VPVersionPicker" id="vp-version-picker">{{ version_picker_groups }}</div>
66+
{%- endif -%}

assets/css/custom.css

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,73 @@ table.resolution > tbody > tr:last-child > td:first-child {
88
text-align: right;
99
}
1010

11+
/* Per-product version picker at the top of the sidebar */
12+
.VPVersionPicker {
13+
margin-bottom: 1rem;
14+
padding-bottom: 0.75rem;
15+
border-bottom: 1px solid var(--vp-c-divider);
16+
}
17+
18+
.VPVersionPickerLabel {
19+
display: block;
20+
font-size: 11px;
21+
font-weight: 600;
22+
text-transform: uppercase;
23+
letter-spacing: 0.4px;
24+
color: var(--vp-c-text-2);
25+
margin-bottom: 0.4rem;
26+
}
27+
28+
.VPVersionPickerList {
29+
list-style: none;
30+
margin: 0;
31+
padding: 0;
32+
display: flex;
33+
flex-wrap: wrap;
34+
gap: 0.4rem;
35+
}
36+
37+
.VPVersionPickerItem {
38+
display: inline-flex;
39+
align-items: center;
40+
gap: 0.3rem;
41+
padding: 0.15rem 0.55rem;
42+
border: 1px solid var(--vp-c-divider);
43+
border-radius: 0.5rem;
44+
font-size: 13px;
45+
font-weight: 500;
46+
color: var(--vp-c-text-2);
47+
text-decoration: none;
48+
transition: border-color 0.2s, background-color 0.2s, color 0.2s;
49+
}
50+
51+
.VPVersionPickerItem:hover {
52+
border-color: var(--vp-c-brand-1);
53+
color: var(--vp-c-brand-1);
54+
}
55+
56+
.VPVersionPickerItem.is-active {
57+
border-color: var(--vp-c-brand-1);
58+
background-color: var(--vp-c-brand-soft);
59+
color: var(--vp-c-brand-1);
60+
}
61+
62+
.VPVersionPickerBadge {
63+
font-size: 10px;
64+
font-weight: 600;
65+
text-transform: uppercase;
66+
letter-spacing: 0.3px;
67+
padding: 0.05rem 0.3rem;
68+
border-radius: 0.4rem;
69+
background-color: var(--vp-c-brand-soft);
70+
color: var(--vp-c-brand-1);
71+
}
72+
73+
.VPVersionPickerItem.is-active .VPVersionPickerBadge {
74+
background-color: var(--vp-c-brand-1);
75+
color: var(--vp-c-white);
76+
}
77+
1178
/* Highlight the active page more visibly */
1279
.VPSidebarItem.level-0.is-active > .item .link > .text,
1380
.VPSidebarItem.level-1.is-active > .item .link > .text,

0 commit comments

Comments
 (0)