Compare commits

..

4 Commits

Author SHA1 Message Date
Ludovic Fernandez
1c06ae5580 chore(scripts): generic way to build scripts (#829)
* chore: move core scripts

* chore: extract head scripts
2025-09-11 20:29:18 +01:00
Ludovic Fernandez
ccb63d60f1 feat(tabs): revamp tabs (#815) 2025-09-10 23:54:27 +01:00
Keith Stockdale
3bc454bbf6 feat: support hiding the main sidebar in desktop site (#778)
* feat: Remove the main sidebar entirely to free up more space for the main content of the page

* fix: ensure that the footer switches are still visible when the main sidebar has been disabled

* refactor: Repurpose Params.sidebar.hide to disable the main sidebar and disable the placeholder rather than adding a new front matter parameter

* fix: change wording from "disable" to "hide" in the documentation for hiding the sidebar

* fix: using incorrect hidden class in sidebar.html broke mobile navigation. Fixed this

---------

Co-authored-by: Xin <5097752+imfing@users.noreply.github.com>
2025-09-10 22:45:12 +01:00
Xin
1b536e27a5 chore(build): update MAIN_VERSION to v0.11.1 and change source directory to docs 2025-09-06 14:03:29 +01:00
24 changed files with 217 additions and 127 deletions

6
assets/js/head/banner.js Normal file
View File

@@ -0,0 +1,6 @@
// The section must not be in the banner.js (body) file because it can create a quick flash.
if (localStorage.getItem('{{ site.Params.banner.key | default `banner-closed` }}')) {
document.documentElement.style.setProperty("--hextra-banner-height", "0px");
document.documentElement.classList.add("hextra-banner-hidden");
}

14
assets/js/head/theme.js Normal file
View File

@@ -0,0 +1,14 @@
// The section must not be in the theme.js (body) file because it can create a quick flash (switch between light and dark).
function setTheme(theme) {
document.documentElement.classList.remove("light", "dark");
if (theme !== "light" && theme !== "dark") {
theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}
document.documentElement.classList.add(theme);
document.documentElement.style.colorScheme = theme;
}
setTheme("color-theme" in localStorage ? localStorage.getItem("color-theme") : '{{ site.Params.theme.default | default `system`}}')

View File

@@ -9,7 +9,7 @@ echo "Using base URL: $BASE_URL"
# Version configuration - modify these arrays to specify versions to build
# MAIN_VERSION format: "ref:display_name:source_dir"
# VERSIONS format: "ref:display_name:source_dir" where source_dir is either "docs" or "exampleSite"
MAIN_VERSION="v0.11.0:latest:exampleSite"
MAIN_VERSION="v0.11.1:latest:docs"
VERSIONS=(
"main:latest:docs" # latest version always builds from main
"v0.10.2:v0.10:exampleSite"

View File

@@ -164,6 +164,21 @@ menu:
weight: 3
```
### Hiding
Hiding the sidebar can be done using front matter:
```yaml {filename="content/docs/guide/configuration.md"}
---
title: Configuration
sidebar:
hide: true
---
```
This will hide the main sidebar from the page, freeing up space for the main content of the page.
## Right Sidebar
### Table of Contents

View File

@@ -5,12 +5,10 @@ next: /docs/guide/deploy-site
## Example
{{< tabs items="macOS,Linux,Windows" >}}
{{< tab >}}**macOS**: A desktop operating system by Apple.{{< /tab >}}
{{< tab >}}**Linux**: An open-source operating system.{{< /tab >}}
{{< tab >}}**Windows**: A desktop operating system by Microsoft.{{< /tab >}}
{{< tabs >}}
{{< tab name="JSON" >}}**JSON**: JavaScript Object Notation (JSON) is a standard text-based format for representing structured data based on JavaScript object syntax.{{< /tab >}}
{{< tab name="YAML" >}}**YAML**: YAML is a human-readable data serialization language.{{< /tab >}}
{{< tab name="TOML" >}}**TOML**: TOML aims to be a minimal configuration file format that's easy to read due to obvious semantics.{{< /tab >}}
{{< /tabs >}}
## Usage
@@ -18,37 +16,35 @@ next: /docs/guide/deploy-site
### Default
```
{{</* tabs items="JSON,YAML,TOML" */>}}
{{</* tabs */>}}
{{</* tab */>}}**JSON**: JavaScript Object Notation (JSON) is a standard text-based format for representing structured data based on JavaScript object syntax.{{</* /tab */>}}
{{</* tab */>}}**YAML**: YAML is a human-readable data serialization language.{{</* /tab */>}}
{{</* tab */>}}**TOML**: TOML aims to be a minimal configuration file format that's easy to read due to obvious semantics.{{</* /tab */>}}
{{</* tab name="JSON" */>}}**JSON**: JavaScript Object Notation (JSON) is a standard text-based format for representing structured data based on JavaScript object syntax.{{</* /tab */>}}
{{</* tab name="YAML" */>}}**YAML**: YAML is a human-readable data serialization language.{{</* /tab */>}}
{{</* tab name="TOML" */>}}**TOML**: TOML aims to be a minimal configuration file format that's easy to read due to obvious semantics.{{</* /tab */>}}
{{</* /tabs */>}}
```
### Specify Selected Index
### Specify Selected Tab
Use `defaultIndex` property to specify the selected tab. The index starts from 0.
Use `selected` property to specify the selected tab.
```
{{</* tabs items="JSON,YAML,TOML" defaultIndex="1" */>}}
{{</* tabs */>}}
{{</* tab */>}}**JSON**: JavaScript Object Notation (JSON) is a standard text-based format for representing structured data based on JavaScript object syntax.{{</* /tab */>}}
{{</* tab */>}}**YAML**: YAML is a human-readable data serialization language.{{</* /tab */>}}
{{</* tab */>}}**TOML**: TOML aims to be a minimal configuration file format that's easy to read due to obvious semantics.{{</* /tab */>}}
{{</* tab name="JSON" */>}}**JSON**: JavaScript Object Notation (JSON) is a standard text-based format for representing structured data based on JavaScript object syntax.{{</* /tab */>}}
{{</* tab name="YAML" selected=true */>}}**YAML**: YAML is a human-readable data serialization language.{{</* /tab */>}}
{{</* tab name="TOML" */>}}**TOML**: TOML aims to be a minimal configuration file format that's easy to read due to obvious semantics.{{</* /tab */>}}
{{</* /tabs */>}}
```
The `YAML` tab will be selected by default.
{{< tabs items="JSON,YAML,TOML" defaultIndex="1" >}}
{{< tab >}}**JSON**: JavaScript Object Notation (JSON) is a standard text-based format for representing structured data based on JavaScript object syntax.{{< /tab >}}
{{< tab >}}**YAML**: YAML is a human-readable data serialization language.{{< /tab >}}
{{< tab >}}**TOML**: TOML aims to be a minimal configuration file format that's easy to read due to obvious semantics.{{< /tab >}}
{{< tabs >}}
{{< tab name="JSON" >}}**JSON**: JavaScript Object Notation (JSON) is a standard text-based format for representing structured data based on JavaScript object syntax.{{< /tab >}}
{{< tab name="YAML" selected=true >}}**YAML**: YAML is a human-readable data serialization language.{{< /tab >}}
{{< tab name="TOML" >}}**TOML**: TOML aims to be a minimal configuration file format that's easy to read due to obvious semantics.{{< /tab >}}
{{< /tabs >}}
@@ -57,9 +53,9 @@ The `YAML` tab will be selected by default.
Markdown syntax including code block is also supported:
````
{{</* tabs items="JSON,YAML,TOML" */>}}
{{</* tabs */>}}
{{</* tab */>}}
{{</* tab name="JSON" */>}}
```json
{ "hello": "world" }
```
@@ -70,21 +66,21 @@ Markdown syntax including code block is also supported:
{{</* /tabs */>}}
````
{{< tabs items="JSON,YAML,TOML" >}}
{{< tabs >}}
{{< tab >}}
{{< tab name="JSON" >}}
```json
{ "hello": "world" }
```
{{< /tab >}}
{{< tab >}}
{{< tab name="YAML" >}}
```yaml
hello: world
```
{{< /tab >}}
{{< tab >}}
{{< tab name="TOML" >}}
```toml
hello = "world"
```
@@ -97,7 +93,7 @@ Markdown syntax including code block is also supported:
Tabs with the same list of `items` can be synchronized. When enabled, selecting a tab updates all other tabs with the same `items` and remembers the selection across pages.
Enable globally in your `hugo.yaml` under the `page` section:
Enable/disable globally in your `hugo.yaml` under the `page` section:
```yaml {filename="hugo.yaml"}
params:
@@ -106,20 +102,33 @@ params:
sync: true
```
With this enabled the following two tab blocks will always display the same selected item:
Enable/disable per page inside the front matter:
```yaml {filename="my_page.md"}
---
title: My page
params:
tabs:
sync: true
---
Example content.
```
With this enabled, the following two tab blocks will always display the same selected item:
```markdown
{{</* tabs items="A,B" */>}}
{{</* tabs */>}}
{{</* tab */>}}A content{{</* /tab */>}}
{{</* tab */>}}B content{{</* /tab */>}}
{{</* tab name="A" */>}}A content{{</* /tab */>}}
{{</* tab name="B" */>}}B content{{</* /tab */>}}
{{</* /tabs */>}}
{{</* tabs items="A,B" */>}}
{{</* tabs */>}}
{{</* tab */>}}Second A content{{</* /tab */>}}
{{</* tab */>}}Second B content{{</* /tab */>}}
{{</* tab name="A" */>}}Second A content{{</* /tab */>}}
{{</* tab name="B" */>}}Second B content{{</* /tab */>}}
{{</* /tabs */>}}
```

View File

@@ -55,32 +55,17 @@
{{ partial "components/analytics/analytics.html" . }}
<script>
// The section must not be in the theme.js file because it can create a quick flash (switch between light and dark).
{{- $scriptsHead := slice -}}
{{- range resources.Match "js/head/*.js" -}}
{{ $scriptsHead = $scriptsHead | append (resources.ExecuteAsTemplate .Name $ .) }}
{{- end -}}
function setTheme(theme) {
document.documentElement.classList.remove("light", "dark");
{{- $scripts := $scriptsHead | resources.Concat "js/main-head.js" -}}
if (theme !== "light" && theme !== "dark") {
theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}
document.documentElement.classList.add(theme);
document.documentElement.style.colorScheme = theme;
}
setTheme("color-theme" in localStorage ? localStorage.getItem("color-theme") : '{{ site.Params.theme.default | default `system`}}')
</script>
<script>
// The section must not be in the banner.js file because it can create a quick flash.
if (localStorage.getItem('{{ site.Params.banner.key | default `banner-closed` }}')) {
document.documentElement.style.setProperty("--hextra-banner-height", "0px");
document.documentElement.classList.add("hextra-banner-hidden");
}
</script>
{{- if hugo.IsProduction -}}
{{- $scripts = $scripts | minify | fingerprint -}}
{{- end -}}
<script src="{{ $scripts.RelPermalink }}" integrity="{{ $scripts.Data.Integrity }}"></script>
<!-- Math engine -->
{{ $noop := .WordCount -}}

View File

@@ -1,18 +1,9 @@
{{- $jsSwitcherMenu := resources.Get "js/switcher-menu.js" -}}
{{- $jsTheme := resources.Get "js/theme.js" | resources.ExecuteAsTemplate "theme.js" . -}}
{{- $jsBanner := resources.Get "js/banner.js" | resources.ExecuteAsTemplate "banner.js" . -}}
{{- $jsMenu := resources.Get "js/menu.js" -}}
{{- $jsTabs := resources.Get "js/tabs.js" -}}
{{- $jsLang := resources.Get "js/lang.js" -}}
{{- $jsNavMenu := resources.Get "js/nav-menu.js" -}}
{{- $jsCodeCopy := resources.Get "js/code-copy.js" -}}
{{- $jsFileTree := resources.Get "js/filetree.js" -}}
{{- $jsSidebar := resources.Get "js/sidebar.js" -}}
{{- $jsBackToTop := resources.Get "js/back-to-top.js" -}}
{{- $jsTocScroll := resources.Get "js/toc-scroll.js" -}}
{{- $jsFavicon := resources.Get "js/favicon.js" | resources.ExecuteAsTemplate "favicon.js" . -}}
{{- $scriptsBody := slice }}
{{- range resources.Match "js/core/*.js" -}}
{{ $scriptsBody = $scriptsBody | append (resources.ExecuteAsTemplate .Name $ .) }}
{{- end -}}
{{- $scripts := slice $jsSwitcherMenu $jsTheme $jsBanner $jsMenu $jsCodeCopy $jsTabs $jsLang $jsNavMenu $jsFileTree $jsSidebar $jsBackToTop $jsTocScroll $jsFavicon | resources.Concat "js/main.js" -}}
{{- $scripts := $scriptsBody | resources.Concat "js/main.js" -}}
{{- if hugo.IsProduction -}}
{{- $scripts = $scripts | minify | fingerprint -}}
{{- end -}}

View File

@@ -0,0 +1,69 @@
{{- $tabsID := .id }}
{{- /*
The `tabs` parameter is a list of dict with the following keys:
- `id`: (int) the ID of the tab (the Ordinal of the tab shortcode).
- `name`: (string) the name of the tab (the title).
- `content`: (string) the content of the tab.
- `selected`: (bool) whether the tab is selected.
*/ -}}
{{- $tabs := .tabs }}
{{- if eq (len $tabs) 0 -}}
{{ errorf "tabs must have at least one tab" }}
{{- end -}}
{{- $enableSync := .enableSync }}
{{- /* Create group data for syncing and select the first tab if none is selected. */ -}}
{{- $selectedIndex := 0 -}}
{{ $dataTabGroup := slice -}}
{{- range $i, $item := $tabs -}}
{{- $dataTabGroup = $dataTabGroup | append ($item.name) -}}
{{- if $item.selected -}}
{{- $selectedIndex = $i -}}
{{- end -}}
{{- end -}}
{{- /* Generate a unique ID for each tab group. */ -}}
{{- $globalID := printf "tabs-%02v" $tabsID -}}
<div class="hextra-scrollbar hx:overflow-x-auto hx:overflow-y-hidden hx:overscroll-x-contain">
<div
class="hx:mt-4 hx:flex hx:w-max hx:min-w-full hx:border-b hx:border-gray-200 hx:pb-px hx:dark:border-neutral-800"
{{ if $enableSync }} data-tab-group="{{ delimit $dataTabGroup `,` }}"{{ end }}
>
{{- range $i, $item := $tabs -}}
<button
class="hextra-tabs-toggle hx:cursor-pointer hx:data-[state=selected]:border-primary-500 hx:data-[state=selected]:text-primary-600 hx:data-[state=selected]:dark:border-primary-500 hx:data-[state=selected]:dark:text-primary-600 hx:mr-2 hx:rounded-t hx:p-2 hx:font-medium hx:leading-5 hx:transition-colors hx:-mb-0.5 hx:select-none hx:border-b-2 hx:border-transparent hx:text-gray-600 hx:hover:border-gray-200 hx:hover:text-black hx:dark:text-gray-200 hx:dark:hover:border-neutral-800 hx:dark:hover:text-white"
role="tab"
type="button"
aria-controls="tabs-panel-{{ $globalID }}-{{ $item.id }}"
{{- if eq $i $selectedIndex -}}
aria-selected="true"
tabindex="0"
data-state="selected"
{{- end }}
>
{{- $item.name -}}
</button>
{{- end -}}
</div>
</div>
<div>
{{- range $i, $item := $tabs -}}
<div
class="hextra-tabs-panel hx:rounded-sm hx:pt-6 hx:hidden hx:data-[state=selected]:block"
id="tabs-panel-{{ $globalID }}-{{ $item.id }}"
role="tabpanel"
{{- if eq $i $selectedIndex -}}
tabindex="0"
data-state="selected"
{{ end -}}
>
{{- $item.content | markdownify -}}
</div>
{{- end -}}
</div>

View File

@@ -3,17 +3,22 @@
{{- $disableSidebar := .disableSidebar | default false -}}
{{- $displayPlaceholder := .displayPlaceholder | default false -}}
{{- $sidebarClass := cond $disableSidebar (cond $displayPlaceholder "hx:md:hidden hx:xl:block" "hx:md:hidden") "hx:md:sticky" -}}
{{- $navRoot := cond (eq site.Home.Type "docs") site.Home $context.FirstSection -}}
{{- $pageURL := $context.RelPermalink -}}
{{/* EXPERIMENTAL */}}
{{- if .context.Params.sidebar.hide -}}
{{- $disableSidebar = true -}}
{{- $displayPlaceholder = true -}}
{{- $displayPlaceholder = false -}}
{{- end -}}
{{- $sidebarClass := "hx:md:sticky" -}}
{{- if $disableSidebar -}}
{{- if $displayPlaceholder -}}
{{- $sidebarClass = "hx:md:hidden hx:xl:block" -}}
{{- else -}}
{{- $sidebarClass = "hx:md:hidden" -}}
{{- end -}}
{{- end -}}
<aside class="hextra-sidebar-container hx:flex hx:flex-col hx:print:hidden hx:md:top-16 hx:md:shrink-0 hx:md:w-64 hx:md:self-start hx:max-md:[transform:translate3d(0,-100%,0)] {{ $sidebarClass }}">
<!-- Search bar on small screen -->

View File

@@ -1,18 +1,24 @@
{{- /*
Create a tab.
@example {{< tab >}}content{{< /tab >}}
@param {string} name The name of the tab.
@param {string} selected Whether the tab is selected.
@example {{< tab name="Foo" selected=true >}}content{{< /tab >}}
*/ -}}
{{- $defaultIndex := int ((.Parent.Get "defaultIndex") | default "0") -}}
{{- $name := .Get "name" | default (printf "Tab %d" .Ordinal) -}}
<div
class="hextra-tabs-panel hx:rounded-sm hx:pt-6 hx:hidden hx:data-[state=selected]:block"
id="tabs-panel-{{ .Ordinal }}"
role="tabpanel"
{{- if eq .Ordinal $defaultIndex }} tabindex="0" {{ end -}}
{{- if eq .Ordinal $defaultIndex }} data-state="selected" {{ end -}}
>
{{- .InnerDeindent | markdownify -}}
</div>
{{- /* Drop trailing newlines */ -}}
{{- $selected := .Get "selected" -}}
{{- if .Parent.Get "defaultIndex" -}}
{{- $selected = eq .Ordinal (int (.Parent.Get "defaultIndex")) -}}
{{- end -}}
{{- $tabs := .Parent.Store.Get "tabs" | default slice -}}
{{ .Parent.Store.Set "tabs" ($tabs | append (dict
"id" .Ordinal
"name" $name
"content" .InnerDeindent
"selected" $selected
))
-}}

View File

@@ -1,49 +1,39 @@
{{- /*
Create a tabbed interface with the given items.
@param {string} items The items to display in the tabs.
@param {string} defaultIndex The index of the default tab.
@example {{< tabs items="JSON,YAML,TOML" >}}{{< /tabs >}}
@example {{< tabs >}}...{{< /tabs >}}
*/ -}}
{{- $items := split (.Get "items") "," -}}
{{- $defaultIndex := int ((.Get "defaultIndex") | default "0") -}}
{{- /* Unused, but required for the shortcode to work. */ -}}
{{- .Inner -}}
{{- $enableSync := site.Params.page.tabs.sync | default false -}}
{{- if not (.Get "items") -}}
{{ errorf "tabs shortcode: 'items' parameter is required" }}
{{- /* Enable syncing of tabs across the page. */ -}}
{{- $enableSync := false -}}
{{- if or (eq .Page.Params.tabs.sync false) (eq .Page.Params.tabs.sync true) -}}
{{- $enableSync = .Page.Params.tabs.sync -}}
{{- else -}}
{{- $enableSync = site.Params.page.tabs.sync | default false -}}
{{- end -}}
{{- if not $items -}}
{{ errorf "tabs shortcode: 'items' parameter cannot be empty" }}
{{- $tabs := ($.Store.Get "tabs") | default slice -}}
{{- /* Compatibility with previous parameter "items". */ -}}
{{- if .Get "defaultIndex" -}}
{{- warnf "The 'defaultIndex' parameter of the 'tabs' shortcode is deprecated. Please use 'selected' on 'tab' instead." -}}
{{- end -}}
{{- range $items -}}
{{- if eq (trim . " ") "" -}}
{{ errorf "tabs shortcode: empty item found in 'items' parameter" }}
{{- if .Get "items" -}}
{{- warnf "The 'items' parameter of the 'tabs' shortcode is deprecated. Please use 'name' on 'tab' instead." -}}
{{- $items := split (.Get "items") "," -}}
{{- $temp := slice -}}
{{- range $i, $item := $items -}}
{{- $tab := index $tabs $i -}}
{{- $temp = $temp | append (merge $tab (dict "name" $item)) -}}
{{- end -}}
{{- $tabs = $temp -}}
{{- end -}}
<div class="hextra-scrollbar hx:overflow-x-auto hx:overflow-y-hidden hx:overscroll-x-contain">
<div class="hx:mt-4 hx:flex hx:w-max hx:min-w-full hx:border-b hx:border-gray-200 hx:pb-px hx:dark:border-neutral-800"{{ if $enableSync }} data-tab-group="{{ delimit $items `,` }}"{{ end }}>
{{- range $i, $item := $items -}}
<button
class="hextra-tabs-toggle hx:cursor-pointer hx:data-[state=selected]:border-primary-500 hx:data-[state=selected]:text-primary-600 hx:data-[state=selected]:dark:border-primary-500 hx:data-[state=selected]:dark:text-primary-600 hx:mr-2 hx:rounded-t hx:p-2 hx:font-medium hx:leading-5 hx:transition-colors hx:-mb-0.5 hx:select-none hx:border-b-2 hx:border-transparent hx:text-gray-600 hx:hover:border-gray-200 hx:hover:text-black hx:dark:text-gray-200 hx:dark:hover:border-neutral-800 hx:dark:hover:text-white"
role="tab"
type="button"
aria-controls="tabs-panel-{{ $i }}"
{{- if eq $i $defaultIndex }} aria-selected="true" {{ end -}}
{{- if eq $i $defaultIndex }} tabindex="0" {{ end -}}
{{- if eq $i $defaultIndex }} data-state="selected"{{ end -}}
>
{{- $item -}}
</button>
{{- end -}}
</div>
</div>
<div>
{{- .Inner -}}
</div>
{{- /* Drop trailing newlines */ -}}
{{- partial "shortcodes/tabs" (dict "tabs" $tabs "enableSync" $enableSync "id" .Ordinal) -}}