feat(navbar): language switcher (#760)

* feat(navbar): language switcher

* docs: add language-switch

* chore: just for the demo

* fix: drop conflicting CSS

* fix: use constant

* fix: pre-existing bug with rtl on mobile

* docs: add comment to explain the algo

* chore: more readable algo

* review

Co-authored-by: Xin <5097752+imfing@users.noreply.github.com>

* feat: different icons

* feat: icon as param

* fix: inconsitency with rtl

* fix: render inside the sidebar

* chore: remove the demo

---------

Co-authored-by: Xin <5097752+imfing@users.noreply.github.com>
This commit is contained in:
Ludovic Fernandez
2025-08-18 00:26:43 +02:00
committed by GitHub
parent b2ff662c8e
commit 363b1e50ff
8 changed files with 72 additions and 6 deletions

View File

@@ -7,11 +7,32 @@
const optionsElement = switcher.nextElementSibling; const optionsElement = switcher.nextElementSibling;
optionsElement.classList.toggle('hx:hidden'); optionsElement.classList.toggle('hx:hidden');
// Calculate position of language options element // Calculate the position of a language options element.
const switcherRect = switcher.getBoundingClientRect(); const switcherRect = switcher.getBoundingClientRect();
const translateY = switcherRect.top - window.innerHeight - 15;
optionsElement.style.transform = `translate3d(${switcherRect.left}px, ${translateY}px, 0)`; // Must be called before optionsElement.clientWidth.
optionsElement.style.minWidth = `${Math.max(switcherRect.width, 50)}px`; optionsElement.style.minWidth = `${Math.max(switcherRect.width, 50)}px`;
const isOnTop = switcher.dataset.location === 'top';
const isRTL = document.body.dir === 'rtl'
// Stuck on the left side of the switcher.
let translateX = switcherRect.left;
if (isOnTop && !isRTL || !isOnTop && isRTL) {
// Stuck on the right side of the switcher.
translateX = switcherRect.right - optionsElement.clientWidth;
}
// Stuck on the top of the switcher.
let translateY = switcherRect.top - window.innerHeight - 15;
if (isOnTop) {
// Stuck on the bottom of the switcher.
translateY = switcherRect.top - window.innerHeight + 180;
}
optionsElement.style.transform = `translate3d(${translateX}px, ${translateY}px, 0)`;
}); });
}); });

View File

@@ -69,6 +69,15 @@ menu:
- name: Theme Toggle - name: Theme Toggle
params: params:
type: theme-toggle type: theme-toggle
label: true # optional, default is false
```
6. مُبدِّل اللغة
```yaml
- name: مُبدِّل اللغة
params:
type: language-switch
label: true # optional, default is false
icon: "globe-alt" # optional, default is "translate"
``` ```
این آیتم‌های منو را می‌توان با تنظیم پارامتر `weight` مرتب کرد. این آیتم‌های منو را می‌توان با تنظیم پارامتر `weight` مرتب کرد.

View File

@@ -69,6 +69,15 @@ menu:
- name: Theme Toggle - name: Theme Toggle
params: params:
type: theme-toggle type: theme-toggle
label: true # optional, default is false
```
6. 言語スイッチャー
```yaml
- name: 言語スイッチャー
params:
type: language-switch
label: true # optional, default is false
icon: "globe-alt" # optional, default is "translate"
``` ```
これらのメニュー項目は `weight` パラメータを設定することで並べ替えられます。 これらのメニュー項目は `weight` パラメータを設定することで並べ替えられます。

View File

@@ -69,6 +69,15 @@ There are different types of menu items:
- name: Theme Toggle - name: Theme Toggle
params: params:
type: theme-toggle type: theme-toggle
label: true # optional, default is false
```
6. Language Switcher
```yaml
- name: Language Switcher
params:
type: language-switch
label: true # optional, default is false
icon: "globe-alt" # optional, default is "translate"
``` ```
These menu items can be sorted by setting the `weight` parameter. These menu items can be sorted by setting the `weight` parameter.

View File

@@ -69,6 +69,15 @@ menu:
- name: Theme Toggle - name: Theme Toggle
params: params:
type: theme-toggle type: theme-toggle
label: true # optional, default is false
```
6. 语言切换器
```yaml
- name: 语言切换器
params:
type: language-switch
label: true # optional, default is false
icon: "globe-alt" # optional, default is "translate"
``` ```
通过设置 `weight` 参数可以调整菜单项的排序。 通过设置 `weight` 参数可以调整菜单项的排序。

View File

@@ -1,4 +1,9 @@
{{- $page := .context -}} {{- $page := .context -}}
{{- $iconName := .iconName | default "globe-alt" -}}
{{- $iconHeight := .iconHeight | default 12 -}}
{{- $location := .location -}}
{{- $class := .class | default "hx:h-7 hx:px-2 hx:text-xs hx:text-gray-600 hx:transition-colors hx:dark:text-gray-400 hx:hover:bg-gray-100 hx:hover:text-gray-900 hx:dark:hover:bg-primary-100/5 hx:dark:hover:text-gray-50" -}}
{{- $grow := .grow -}} {{- $grow := .grow -}}
{{- $hideLabel := .hideLabel | default false -}} {{- $hideLabel := .hideLabel | default false -}}
@@ -10,12 +15,13 @@
<button <button
title="{{ $changeLanguage }}" title="{{ $changeLanguage }}"
data-state="closed" data-state="closed"
class="hextra-language-switcher hx:cursor-pointer hx:h-7 hx:rounded-md hx:px-2 hx:text-left hx:text-xs hx:font-medium hx:text-gray-600 hx:transition-colors hx:dark:text-gray-400 hx:hover:bg-gray-100 hx:hover:text-gray-900 hx:dark:hover:bg-primary-100/5 hx:dark:hover:text-gray-50 hx:grow" data-location="{{ $location }}"
class="hextra-language-switcher hx:cursor-pointer hx:rounded-md hx:text-left hx:font-medium {{ $class }} hx:grow"
type="button" type="button"
aria-label="{{ $changeLanguage }}" aria-label="{{ $changeLanguage }}"
> >
<div class="hx:flex hx:items-center hx:gap-2 hx:capitalize"> <div class="hx:flex hx:items-center hx:gap-2 hx:capitalize">
{{- partial "utils/icon" (dict "name" "globe-alt" "attributes" "height=12") -}} {{- partial "utils/icon" (dict "name" $iconName "attributes" (printf "height=%d" $iconHeight)) -}}
{{- if not $hideLabel }}<span>{{ site.Language.LanguageName }}</span>{{ end -}} {{- if not $hideLabel }}<span>{{ site.Language.LanguageName }}</span>{{ end -}}
</div> </div>
</button> </button>

View File

@@ -7,6 +7,7 @@
{{ end -}} {{ end -}}
{{- end -}} {{- end -}}
{{- $page := . -}}
{{- $iconHeight := 24 -}} {{- $iconHeight := 24 -}}
<div class="hextra-nav-container hx:sticky hx:top-0 hx:z-20 hx:w-full hx:bg-transparent hx:print:hidden"> <div class="hextra-nav-container hx:sticky hx:top-0 hx:z-20 hx:w-full hx:bg-transparent hx:print:hidden">
@@ -39,6 +40,8 @@
</a> </a>
{{- else if eq .Params.type "theme-toggle" -}} {{- else if eq .Params.type "theme-toggle" -}}
{{- partial "theme-toggle.html" (dict "iconHeight" $iconHeight "class" "hx:p-2" "hideLabel" (not .Params.label)) -}} {{- partial "theme-toggle.html" (dict "iconHeight" $iconHeight "class" "hx:p-2" "hideLabel" (not .Params.label)) -}}
{{- else if eq .Params.type "language-switch" -}}
{{- partial "language-switch" (dict "context" $page "grow" false "hideLabel" (not .Params.label) "iconName" (.Params.icon | default "translate") "iconHeight" $iconHeight "location" "top" "class" "hx:p-2") -}}
{{- else -}} {{- else -}}
{{- $active := or ($currentPage.HasMenuCurrent "main" .) ($currentPage.IsMenuCurrent "main" .) -}} {{- $active := or ($currentPage.HasMenuCurrent "main" .) ($currentPage.IsMenuCurrent "main" .) -}}
{{- $activeClass := cond $active "hx:font-medium" "hx:text-gray-600 hx:hover:text-gray-800 hx:dark:text-gray-400 hx:dark:hover:text-gray-200" -}} {{- $activeClass := cond $active "hx:font-medium" "hx:text-gray-600 hx:hover:text-gray-800 hx:dark:text-gray-400 hx:dark:hover:text-gray-200" -}}

View File

@@ -1,6 +1,6 @@
{{- $hideLabel := .hideLabel -}} {{- $hideLabel := .hideLabel -}}
{{- $iconHeight := .iconHeight | default 12 -}} {{- $iconHeight := .iconHeight | default 12 -}}
{{- $class := .class | default "hx:px-2 hx:text-xs hx:hover:bg-gray-100 hx:hover:text-gray-900 hx:dark:hover:bg-primary-100/5 hx:dark:hover:text-gray-50 hx:font-medium hx:text-gray-600 hx:transition-colors hx:dark:text-gray-400" -}} {{- $class := .class | default "hx:h-7 hx:px-2 hx:text-xs hx:hover:bg-gray-100 hx:hover:text-gray-900 hx:dark:hover:bg-primary-100/5 hx:dark:hover:text-gray-50 hx:font-medium hx:text-gray-600 hx:transition-colors hx:dark:text-gray-400" -}}
{{- $changeTheme := (T "changeTheme") | default "Change theme" -}} {{- $changeTheme := (T "changeTheme") | default "Change theme" -}}
{{- $light := (T "light") | default "Light" -}} {{- $light := (T "light") | default "Light" -}}