Skip to content
203 changes: 203 additions & 0 deletions src/components/GlobalLanguageSelector.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
---
import Nodejs from '../assets/icons/nodejs.svg';
import Go from '../assets/icons/go.svg';
import Python from '../assets/icons/python.svg';

const languageOptions = [
{ value: 'js', label: 'JavaScript', icon: Nodejs },
{ value: 'go', label: 'Go', icon: Go },
{ value: 'python', label: 'Python (Preview)', icon: Python },
];
---

<div class="global-language-selector">
<label for="global-language-select" class="sr-only">Select programming language</label>

<!-- Display current language (CSS-controlled, no flicker) -->
<div class="language-display">
{languageOptions.map(({ value, label, icon: IconComponent }) => (
<span class="lang-display" data-lang={value}>
<IconComponent class="lang-icon" />
<span class="lang-label">{label}</span>
</span>
))}
<svg class="dropdown-arrow" width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 9L1 4h10z" fill="currentColor"/>
</svg>
</div>

<!-- Hidden select for functionality -->
<select
id="global-language-select"
class="language-dropdown-hidden"
aria-label="Select programming language"
>
{languageOptions.map(({ value, label }) => (
<option value={value}>
{label}
</option>
))}
</select>
</div>

<script>
/**
* Global Language Selector
* Syncs with page-level selectors via UnifiedPageManager
*/

// Type declaration for UnifiedPageManager
declare global {
interface Window {
unifiedPageManager?: {
setLanguagePublic: (lang: string) => void;
getCurrentLanguage: () => string;
};
}
}

document.addEventListener('DOMContentLoaded', () => {
const displayContainer = document.querySelector('.language-display') as HTMLElement;
const hiddenSelect = document.getElementById('global-language-select') as HTMLSelectElement;

if (!displayContainer || !hiddenSelect) return;

// Sync hidden select value with current language (for dropdown checkmark)
function syncSelectValue() {
const currentLang = document.documentElement.getAttribute('data-genkit-lang') || 'js';
if (hiddenSelect.value !== currentLang) {
hiddenSelect.value = currentLang;
}
}

// Initial sync
syncSelectValue();

// Watch for language changes from page-level selector
const observer = new MutationObserver(() => {
syncSelectValue();
});

observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['data-genkit-lang']
});

// Click on display opens the hidden select
displayContainer.addEventListener('click', () => {
hiddenSelect.focus();
hiddenSelect.click();
});

// Handle selection changes - UnifiedPageManager handles the rest
hiddenSelect.addEventListener('change', (e) => {
const newLang = (e.target as HTMLSelectElement).value;
if (window.unifiedPageManager) {
window.unifiedPageManager.setLanguagePublic(newLang);
}
});
});
</script>

<style>
.global-language-selector {
padding: 0.75rem 1rem;
margin-bottom: 0.5rem;
position: relative;
}

.language-display {
width: 100%;
max-width: 100%;
padding: 0.75rem 1rem;
background: var(--sl-color-bg-nav);
border: 1px solid var(--sl-color-gray-5);
border-radius: 0.5rem;
color: var(--sl-color-white);
font-size: 0.9375rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
min-height: 2.75rem;
}

.language-display:hover {
border-color: var(--sl-color-accent);
background-color: var(--sl-color-gray-6);
}

.language-display:focus-within {
outline: 2px solid var(--sl-color-accent);
outline-offset: 2px;
}

/* Hide all language displays by default */
.lang-display {
display: none;
align-items: center;
gap: 0.625rem;
}

/* Show only the current language (CSS-controlled, no flicker!) */
html[data-genkit-lang="js"] .lang-display[data-lang="js"],
html[data-genkit-lang="go"] .lang-display[data-lang="go"],
html[data-genkit-lang="python"] .lang-display[data-lang="python"] {
display: flex;
}

.lang-icon {
width: 1.25rem;
height: 1.25rem;
flex-shrink: 0;
}

.lang-label {
flex: 1;
}

.dropdown-arrow {
flex-shrink: 0;
margin-left: 0.5rem;
}

/* Hidden select for functionality */
.language-dropdown-hidden {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
font-size: 0.875rem;
}

/* Mobile adjustments */
@media (max-width: 50rem) {
.global-language-selector {
padding: 0.75rem 1rem;
}

.language-display {
font-size: 1rem;
padding: 0.75rem 1rem;
}
}

/* Screen reader only */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
</style>
14 changes: 5 additions & 9 deletions src/components/LanguageSelector.astro
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
---
export interface Props {
languages?: string[] | string;
supportedLanguages?: string[] | string;
}

const { languages = ['js', 'go', 'python'], supportedLanguages = ['js', 'go', 'python'] } = Astro.props;

const currentLang = Astro.url.searchParams.get('lang') || 'js';
const { supportedLanguages = ['js', 'go', 'python'] } = Astro.props;

const languageLabels: Record<string, string> = {
js: 'JavaScript',
Expand All @@ -22,10 +19,7 @@ function parseLanguages(input: string[] | string): string[] {
return input.trim().split(/\s+/).filter(Boolean);
}

// Use supportedLanguages if provided, otherwise fall back to languages prop
const displayLanguages = parseLanguages(supportedLanguages);
const allLanguages = ['js', 'go', 'python'];
const hasLimitedSupport = displayLanguages.length < allLanguages.length;
---

<div class="language-selector" role="tablist" aria-label="Choose programming language">
Expand All @@ -34,7 +28,6 @@ const hasLimitedSupport = displayLanguages.length < allLanguages.length;
<button
class="lang-pill"
role="tab"
aria-selected={currentLang === lang ? 'true' : 'false'}
aria-controls={`content-${lang}`}
data-lang={lang}
>
Expand Down Expand Up @@ -79,7 +72,10 @@ const hasLimitedSupport = displayLanguages.length < allLanguages.length;
border-color: var(--sl-color-accent);
}

.lang-pill[aria-selected='true'] {
/* CSS-based active state - no flicker! */
html[data-genkit-lang="js"] .lang-pill[data-lang="js"],
html[data-genkit-lang="go"] .lang-pill[data-lang="go"],
html[data-genkit-lang="python"] .lang-pill[data-lang="python"] {
background-color: var(--sl-color-accent);
color: white;
border-color: var(--sl-color-accent);
Expand Down
3 changes: 3 additions & 0 deletions src/components/sidebar.astro
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
---
import GlobalLanguageSelector from './GlobalLanguageSelector.astro';
import MobileMenuFooter from '@astrojs/starlight/components/MobileMenuFooter.astro';
import SidebarPersister from '@astrojs/starlight/components/SidebarPersister.astro';
import SidebarSublist from '@astrojs/starlight/components/SidebarSublist.astro';

const { sidebar } = Astro.locals.starlightRoute;
---

<GlobalLanguageSelector />

<SidebarPersister>
<SidebarSublist sublist={sidebar} />
</SidebarPersister>
Expand Down
36 changes: 28 additions & 8 deletions src/content/custom/head.astro
Original file line number Diff line number Diff line change
Expand Up @@ -256,27 +256,47 @@ html.scroll-restoring .right-sidebar {
opacity: 1 !important;
}

/* Hide ALL language content by default */
/* ============================================
LANGUAGE-BASED FILTERING
Content & Navigation controlled by html[data-genkit-lang] attribute
============================================ */

/* --- Content Filtering --- */
/* Hide all language-specific content by default */
.lang-content {
display: none !important;
visibility: hidden !important;
}

/* Show content for the current language (single or multi-language blocks) */
/* Show content for the current language */
/* Using ~= selector to match space-separated values in data-lang attribute */
html[data-genkit-lang="js"] .lang-content[data-lang~="js"] {
html[data-genkit-lang="js"] .lang-content[data-lang~="js"],
html[data-genkit-lang="go"] .lang-content[data-lang~="go"],
html[data-genkit-lang="python"] .lang-content[data-lang~="python"] {
display: block !important;
visibility: visible !important;
}

html[data-genkit-lang="go"] .lang-content[data-lang~="go"] {
display: block !important;
visibility: visible !important;
/* --- Sidebar Navigation Filtering --- */
/* Hide all language-tagged sidebar items by default */
.sidebar-content a[data-lang] {
display: none !important;
transition: opacity 0.15s ease-out;
}

html[data-genkit-lang="python"] .lang-content[data-lang~="python"] {
/* Show sidebar items that support the current language */
html[data-genkit-lang="js"] .sidebar-content a[data-lang~="js"],
html[data-genkit-lang="go"] .sidebar-content a[data-lang~="go"],
html[data-genkit-lang="python"] .sidebar-content a[data-lang~="python"] {
display: block !important;
visibility: visible !important;
}

/* Hide section headers when they have no visible children for current language */
/* Uses :has() to check if section contains any links for the current language */
html[data-genkit-lang="js"] .sidebar-content details:not(:has(a[data-lang~="js"], a:not([data-lang]))),
html[data-genkit-lang="go"] .sidebar-content details:not(:has(a[data-lang~="go"], a:not([data-lang]))),
html[data-genkit-lang="python"] .sidebar-content details:not(:has(a[data-lang~="python"], a:not([data-lang]))) {
display: none !important;
}

</style>
Loading