jan/website/src/components/CustomNav.astro

662 lines
18 KiB
Plaintext

---
// Custom navigation component to add Products and API Reference links
// This overrides the default Starlight Header component
import Search from '@astrojs/starlight/components/Search.astro';
import ThemeSelect from '@astrojs/starlight/components/ThemeSelect.astro';
import { Icon } from '@astrojs/starlight/components';
// Determine if we're on a docs page based on the current path
const currentPath = Astro.url.pathname;
const isDocsPage = currentPath.startsWith('/jan/') ||
currentPath.startsWith('/mobile/') ||
currentPath.startsWith('/server/') ||
currentPath.startsWith('/local-server/') ||
currentPath === '/' ||
currentPath === '/index' ||
currentPath === '/docs' ||
currentPath === '/docs/';
---
<div class="sl-nav-wrapper">
<nav class="sl-nav" aria-label="Main">
<!-- Left side with title and links -->
<div class="sl-nav__left">
<!-- Site title/logo -->
<a href="https://jan.ai" class="sl-nav__title">
👋 Jan
</a>
<!-- Main navigation links - hide on mobile for docs pages -->
<div class={`sl-nav__links ${isDocsPage ? 'docs-page' : 'custom-page'}`}>
<a href="/products" class="sl-nav__link" data-nav-item="products">
Products
</a>
<a href="/jan" class="sl-nav__link" data-nav-item="docs">
Docs
</a>
<a href="/handbook" class="sl-nav__link" data-nav-item="handbook">
Handbook
</a>
<a href="/blog" class="sl-nav__link" data-nav-item="blog">
Blog
</a>
</div>
</div>
<!-- Center search -->
<div class="sl-nav__center">
<div class="sl-nav__search">
<Search />
</div>
</div>
<!-- Right side items -->
<div class="sl-nav__end">
<!-- Changelog and API Reference links -->
<a href="/changelog" class={`sl-nav__link sl-nav__api-link ${isDocsPage ? 'docs-page' : 'custom-page'}`} data-nav-item="changelog">
Changelog
</a>
<a href="/api-reference" class={`sl-nav__link sl-nav__api-link ${isDocsPage ? 'docs-page' : 'custom-page'}`} data-nav-item="api">
API Reference
</a>
<!-- Theme toggle - always visible on desktop -->
<div class="sl-nav__theme">
<ThemeSelect />
</div>
<!-- Social links -->
<div class="sl-nav__social">
<a href="https://github.com/menloresearch/jan" class="sl-nav__social-link" aria-label="GitHub">
<Icon name="github"/>
</a>
<a href="https://twitter.com/jandotai" class="sl-nav__social-link" aria-label="X">
<Icon name="x.com"/>
</a>
<a href="https://discord.com/invite/FTk2MvZwJH" class="sl-nav__social-link" aria-label="Discord">
<Icon name="discord"/>
</a>
</div>
<!-- Mobile hamburger menu - only for custom pages -->
{!isDocsPage && (
<div class="sl-nav__hamburger">
<button id="hamburger-btn" class="hamburger-button" aria-label="Toggle navigation menu">
<span class="hamburger-icon">☰</span>
</button>
<div id="hamburger-menu" class="hamburger-menu">
<a href="/products" class="hamburger-link">Products</a>
<a href="/jan" class="hamburger-link">Docs</a>
<a href="/handbook" class="hamburger-link">Handbook</a>
<a href="/blog" class="hamburger-link">Blog</a>
<a href="/changelog" class="hamburger-link">Changelog</a>
<a href="/api-reference" class="hamburger-link">API Reference</a>
<div class="hamburger-social">
<a href="https://github.com/menloresearch/jan" class="hamburger-social-link" aria-label="GitHub">
<Icon name="github"/>
</a>
<a href="https://twitter.com/jandotai" class="hamburger-social-link" aria-label="X">
<Icon name="x.com"/>
</a>
<a href="https://discord.com/invite/FTk2MvZwJH" class="hamburger-social-link" aria-label="Discord">
<Icon name="discord"/>
</a>
</div>
<div class="hamburger-theme">
<ThemeSelect />
</div>
</div>
</div>
)}
</div>
</nav>
</div>
<style>
/* Base navigation styles */
.sl-nav-wrapper {
position: sticky;
top: 0;
z-index: 999; /* High z-index to stay above ToC and other elements */
background: var(--sl-color-bg-nav);
border-bottom: 1px solid var(--sl-color-hairline);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.sl-nav {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.75rem 1rem;
max-width: 100%;
margin: 0 auto;
height: var(--sl-nav-height, 3.5rem);
}
.sl-nav__center {
flex: 1;
display: flex;
justify-content: center;
max-width: 600px;
margin: 0 1rem;
}
.sl-nav__left {
display: flex;
align-items: center;
gap: 1.5rem;
}
.sl-nav__title {
font-size: 1.125rem;
font-weight: 600;
color: var(--sl-color-white);
text-decoration: none;
display: flex;
align-items: center;
white-space: nowrap;
}
.sl-nav__title:hover {
color: var(--sl-color-text-accent);
}
.sl-nav__links {
display: flex;
align-items: center;
gap: 1.5rem;
}
.sl-nav__link {
color: var(--sl-color-gray-3);
text-decoration: none;
font-size: 0.9375rem;
transition: color 0.2s ease;
white-space: nowrap;
}
.sl-nav__link:hover {
color: var(--sl-color-white);
text-decoration: none;
}
/* Light theme adjustments - better contrast */
html[data-theme="light"] .sl-nav__link {
color: #374151; /* Darker gray for better readability */
}
html[data-theme="light"] .sl-nav__link:hover {
color: var(--sl-color-accent);
}
html[data-theme="light"] .sl-nav__title {
color: #111827; /* Very dark gray, almost black */
}
/* Active link styling */
.sl-nav__link.active {
color: var(--sl-color-text-accent);
font-weight: 500;
}
/* Light theme active link */
html[data-theme="light"] .sl-nav__link.active {
color: var(--sl-color-text-accent);
}
.sl-nav__end {
display: flex;
align-items: center;
gap: 1rem;
}
.sl-nav__search {
width: 100%;
max-width: 400px;
position: relative;
}
/* Fix search input styling for light mode */
.sl-nav__search :global(input) {
width: 100%;
padding: 0.5rem 1rem;
}
/* Fix search bar in light mode with more specific selectors */
html[data-theme="light"] .sl-nav__search :global(.pagefind-ui__search-input),
html[data-theme="light"] .sl-nav__search :global(input[type="search"]),
html[data-theme="light"] .sl-nav__search :global(input) {
background: #f9fafb !important;
background-color: #f9fafb !important;
border: 1px solid #d1d5db !important;
color: #111827 !important;
}
html[data-theme="light"] .sl-nav__search :global(.pagefind-ui__search-input:focus),
html[data-theme="light"] .sl-nav__search :global(input[type="search"]:focus),
html[data-theme="light"] .sl-nav__search :global(input:focus) {
background: #ffffff !important;
background-color: #ffffff !important;
border-color: var(--sl-color-accent) !important;
color: #111827 !important;
}
html[data-theme="light"] .sl-nav__search :global(.pagefind-ui__search-input::placeholder),
html[data-theme="light"] .sl-nav__search :global(input::placeholder) {
color: #9ca3af !important;
}
.sl-nav__social {
display: flex;
align-items: center;
gap: 0.5rem;
}
.sl-nav__social-link {
display: flex;
align-items: center;
padding: 0.375rem;
color: var(--sl-color-gray-3);
transition: color 0.2s ease;
text-decoration: none;
}
.sl-nav__social-link:hover {
color: var(--sl-color-white);
text-decoration: none;
}
/* Light theme social links - better contrast */
html[data-theme="light"] .sl-nav__social-link {
color: #6b7280; /* Medium gray for icons */
}
html[data-theme="light"] .sl-nav__social-link:hover {
color: var(--sl-color-accent);
}
/* Theme selector container */
.sl-nav__theme {
display: flex;
align-items: center;
}
/* Hide only the "Select theme" label text, keep dropdown visible */
.sl-nav__theme :global(starlight-theme-select) {
position: relative;
}
/* Hide only the screen reader text */
.sl-nav__theme :global(starlight-theme-select .sr-only) {
position: absolute !important;
width: 1px !important;
height: 1px !important;
padding: 0 !important;
margin: -1px !important;
overflow: hidden !important;
clip: rect(0, 0, 0, 0) !important;
white-space: nowrap !important;
border: 0 !important;
}
/* Style the select dropdown with visible text */
.sl-nav__theme :global(starlight-theme-select select) {
font-size: 0.875rem !important;
color: var(--sl-color-text) !important;
background: transparent !important;
border: none !important;
cursor: pointer !important;
padding-right: 1.5rem !important;
}
/* Ensure options are visible with proper contrast */
.sl-nav__theme :global(starlight-theme-select option) {
font-size: 0.875rem !important;
color: var(--sl-color-black) !important;
background: var(--sl-color-white) !important;
}
/* Light mode select text color */
html[data-theme="light"] .sl-nav__theme :global(starlight-theme-select select) {
color: var(--sl-color-gray-5) !important;
}
/* Hide the icon to prevent overlap with text */
.sl-nav__theme :global(starlight-theme-select svg) {
display: none;
}
/* Hamburger menu styles */
.sl-nav__hamburger {
display: none;
position: relative; /* Changed from static to contain the menu properly */
}
.hamburger-button {
background: transparent;
border: none;
color: var(--sl-color-white);
font-size: 1.5rem;
cursor: pointer;
padding: 0.375rem;
display: flex;
align-items: center;
justify-content: center;
transition: color 0.2s ease;
}
/* Fix hamburger button visibility in light mode */
html[data-theme="light"] .hamburger-button {
color: #374151; /* Darker gray for better visibility */
}
.hamburger-button:hover {
color: var(--sl-color-text-accent);
}
html[data-theme="light"] .hamburger-button:hover {
color: var(--sl-color-text-accent);
}
.hamburger-menu {
display: none;
position: absolute;
top: calc(100% + 0.5rem); /* Add small gap below navbar */
right: 0;
background: var(--sl-color-bg-nav);
border: 1px solid var(--sl-color-hairline);
border-radius: 0.5rem;
padding: 1rem;
min-width: 200px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 1000; /* Ensure menu stays above other content */
}
/* Light mode menu background */
html[data-theme="light"] .hamburger-menu {
background: var(--sl-color-bg);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.hamburger-menu.show {
display: block;
}
.hamburger-link {
display: block;
padding: 0.5rem 1rem;
color: var(--sl-color-gray-3);
text-decoration: none;
transition: color 0.2s ease, background 0.2s ease;
border-radius: 0.25rem;
}
/* Light mode hamburger links */
html[data-theme="light"] .hamburger-link {
color: #374151; /* Consistent with nav links */
}
.hamburger-link:hover {
color: var(--sl-color-white);
background: var(--sl-color-gray-6);
}
html[data-theme="light"] .hamburger-link:hover {
color: var(--sl-color-accent);
background: var(--sl-color-gray-6);
}
.hamburger-social {
display: flex;
gap: 0.5rem;
padding: 0.5rem 1rem;
margin-top: 0.5rem;
border-top: 1px solid var(--sl-color-hairline);
}
.hamburger-social-link {
display: flex;
align-items: center;
padding: 0.375rem;
color: var(--sl-color-gray-3);
transition: color 0.2s ease;
text-decoration: none;
}
html[data-theme="light"] .hamburger-social-link {
color: #6b7280; /* Consistent with nav social links */
}
.hamburger-social-link:hover {
color: var(--sl-color-white);
}
html[data-theme="light"] .hamburger-social-link:hover {
color: var(--sl-color-accent);
}
.hamburger-theme {
padding: 0.5rem 1rem;
margin-top: 0.5rem;
border-top: 1px solid var(--sl-color-hairline);
}
/* Search component integration styles */
.sl-nav__search :global(.pagefind-ui) {
--pagefind-ui-scale: 0.9;
}
.sl-nav__search :global(.pagefind-ui__search-input) {
background: var(--sl-color-gray-6);
border: 1px solid var(--sl-color-gray-5);
color: var(--sl-color-white);
border-radius: 0.375rem;
font-size: 0.875rem;
padding: 0.375rem 1rem;
transition: border-color 0.2s ease, background 0.2s ease;
}
/* Override any Starlight defaults for search in dark mode */
html[data-theme="dark"] .sl-nav__search :global(.pagefind-ui__search-input),
html[data-theme="dark"] .sl-nav__search :global(input[type="search"]),
html[data-theme="dark"] .sl-nav__search :global(input) {
background: #1f2937 !important;
background-color: #1f2937 !important;
border: 1px solid #374151 !important;
color: #f3f4f6 !important;
}
.sl-nav__search :global(.pagefind-ui__search-input:hover) {
border-color: var(--sl-color-gray-4);
background: var(--sl-color-gray-5);
}
.sl-nav__search :global(.pagefind-ui__search-input:focus) {
outline: none;
border-color: var(--sl-color-text-accent);
background: var(--sl-color-bg);
}
.sl-nav__search :global(.pagefind-ui__search-input::placeholder) {
color: var(--sl-color-gray-3);
}
/* Responsive styles */
/* Tablet view */
@media (max-width: 900px) {
.sl-nav__links {
gap: 1rem;
}
.sl-nav__link {
font-size: 0.875rem;
}
.sl-nav__social {
display: none;
}
}
/* Mobile view - differentiate between docs and custom pages */
@media (max-width: 768px) {
.sl-nav {
padding: 0.5rem 1rem;
}
.sl-nav__center {
margin: 0 0.5rem;
}
/* For docs pages - hide custom nav items on mobile, let Starlight handle it */
.sl-nav__links.docs-page {
display: none;
}
.sl-nav__api-link.docs-page {
display: none;
}
/* For custom pages - hide nav items but show hamburger */
.sl-nav__links.custom-page {
display: none;
}
.sl-nav__api-link.custom-page {
display: none;
}
.sl-nav__theme {
display: none;
}
/* Show hamburger only for custom pages */
.sl-nav__hamburger {
display: block;
}
}
/* Small mobile */
@media (max-width: 640px) {
.sl-nav {
gap: 0.5rem;
}
.sl-nav__left {
gap: 0.5rem;
}
.sl-nav__center {
flex: 1;
margin: 0 0.5rem;
}
.sl-nav__end {
gap: 0.5rem;
}
.sl-nav__title {
font-size: 1rem;
}
.sl-nav__search :global(.pagefind-ui__search-input) {
padding: 0.25rem 0.75rem;
font-size: 0.8125rem;
}
}
/* Make sure content doesn't jump */
:global(body) {
padding-top: 0;
}
:global(.sl-layout) {
padding-top: 0;
}
/* Adjust main content positioning */
:global(main.sl-content) {
margin-top: 0;
}
:global(.sl-content__inner) {
padding-top: 1rem;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Add active state highlighting based on current page
const currentPath = window.location.pathname;
const links = document.querySelectorAll('.sl-nav__link');
links.forEach(link => {
const href = link.getAttribute('href');
if (href) {
// Check for exact match or if current path starts with the link href
if (currentPath === href ||
(href !== '/' && currentPath.startsWith(href))) {
link.classList.add('active');
}
}
});
// Theme handling for custom pages
// Listen for theme changes from Starlight's ThemeSelect
const observeThemeChanges = () => {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.attributeName === 'data-theme') {
const newTheme = document.documentElement.getAttribute('data-theme');
// Theme is already set by Starlight, we just need to ensure it persists
localStorage.setItem('starlight-theme', newTheme === 'dark' ? 'dark' : 'light');
}
});
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['data-theme']
});
};
observeThemeChanges();
// Hamburger menu functionality (only for custom pages)
const hamburgerBtn = document.getElementById('hamburger-btn');
const hamburgerMenu = document.getElementById('hamburger-menu');
if (hamburgerBtn && hamburgerMenu) {
// Toggle menu on button click
hamburgerBtn.addEventListener('click', function(e) {
e.stopPropagation();
hamburgerMenu.classList.toggle('show');
});
// Close menu when clicking outside
document.addEventListener('click', function(e) {
if (!hamburgerBtn.contains(e.target) && !hamburgerMenu.contains(e.target)) {
hamburgerMenu.classList.remove('show');
}
});
// Close menu when clicking a link
const hamburgerLinks = hamburgerMenu.querySelectorAll('.hamburger-link');
hamburgerLinks.forEach(link => {
link.addEventListener('click', function() {
hamburgerMenu.classList.remove('show');
});
});
// Close menu on escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && hamburgerMenu.classList.contains('show')) {
hamburgerMenu.classList.remove('show');
}
});
}
});
</script>