|
| 1 | +--- |
| 2 | +// Custom Starlight header: default chrome (title / search / social / theme / |
| 3 | +// language) plus a desktop-only hover-dropdown nav in the liquid-glass grammar. |
| 4 | +// Mobile (<72rem) hides the nav; the built-in hamburger flow is untouched. |
| 5 | +import LanguageSelect from '@astrojs/starlight/components/LanguageSelect.astro'; |
| 6 | +import Search from '@astrojs/starlight/components/Search.astro'; |
| 7 | +import SiteTitle from '@astrojs/starlight/components/SiteTitle.astro'; |
| 8 | +import SocialIcons from '@astrojs/starlight/components/SocialIcons.astro'; |
| 9 | +import ThemeSelect from '@astrojs/starlight/components/ThemeSelect.astro'; |
| 10 | +
|
| 11 | +const { locale } = Astro.locals.starlightRoute; |
| 12 | +// Root locale is `undefined`; BASE_URL already ends with a slash. |
| 13 | +const base = import.meta.env.BASE_URL.replace(/\/?$/, '/'); |
| 14 | +const prefix = locale ? `${base}${locale}/` : base; |
| 15 | +
|
| 16 | +type NavLink = { label: string; href: string }; |
| 17 | +type NavGroup = { label: string; links: NavLink[] }; |
| 18 | +
|
| 19 | +const t = (en: string, ko: string, zh: string) => |
| 20 | + locale === 'ko' ? ko : locale === 'zh-cn' ? zh : en; |
| 21 | +
|
| 22 | +const groups: NavGroup[] = [ |
| 23 | + { |
| 24 | + label: t('Getting Started', '시작하기', '开始使用'), |
| 25 | + links: [ |
| 26 | + { label: t('Installation', '설치', '安装'), href: `${prefix}getting-started/installation/` }, |
| 27 | + { label: t('Quickstart', '빠른 시작', '快速开始'), href: `${prefix}getting-started/quickstart/` }, |
| 28 | + { label: t('How It Works', '동작 원리', '工作原理'), href: `${prefix}getting-started/how-it-works/` }, |
| 29 | + ], |
| 30 | + }, |
| 31 | + { |
| 32 | + label: t('Guides', '가이드', '指南'), |
| 33 | + links: [ |
| 34 | + { label: t('Providers', '프로바이더', '提供商'), href: `${prefix}guides/providers/` }, |
| 35 | + { label: t('Model Routing', '모델 라우팅', '模型路由'), href: `${prefix}guides/model-routing/` }, |
| 36 | + { label: t('Codex Integration', 'Codex 통합', 'Codex 集成'), href: `${prefix}guides/codex-integration/` }, |
| 37 | + { label: t('Codex App Model Picker', 'Codex App 모델 선택기', 'Codex App 模型选择器'), href: `${prefix}guides/codex-app-models/` }, |
| 38 | + { label: t('Sidecars: Search & Vision', '사이드카: 검색 & 비전', '边车:搜索与视觉'), href: `${prefix}guides/sidecars/` }, |
| 39 | + { label: t('Web Dashboard', '웹 대시보드', '网页控制台'), href: `${prefix}guides/web-dashboard/` }, |
| 40 | + { label: t('Sub-agent Surface', '서브에이전트 서피스', '子代理界面'), href: `${prefix}guides/sub-agent-surface/` }, |
| 41 | + ], |
| 42 | + }, |
| 43 | + { |
| 44 | + label: t('Reference', '레퍼런스', '参考'), |
| 45 | + links: [ |
| 46 | + { label: 'CLI', href: `${prefix}reference/cli/` }, |
| 47 | + { label: t('Configuration', '설정', '配置'), href: `${prefix}reference/configuration/` }, |
| 48 | + { label: t('Adapters', '어댑터', '适配器'), href: `${prefix}reference/adapters/` }, |
| 49 | + { label: t('Architecture', '아키텍처', '架构'), href: `${prefix}reference/architecture/` }, |
| 50 | + ], |
| 51 | + }, |
| 52 | +]; |
| 53 | +
|
| 54 | +const contributing: NavLink = { |
| 55 | + label: t('Contributing', '기여하기', '贡献'), |
| 56 | + href: `${prefix}contributing/`, |
| 57 | +}; |
| 58 | +--- |
| 59 | + |
| 60 | +<div class="header"> |
| 61 | + <div class="title-wrapper sl-flex"> |
| 62 | + <SiteTitle /> |
| 63 | + </div> |
| 64 | + <nav class="glass-nav print:hidden" aria-label={t('Main', '주 메뉴', '主导航')}> |
| 65 | + <ul> |
| 66 | + { |
| 67 | + groups.map((group, i) => ( |
| 68 | + <li class="nav-item"> |
| 69 | + <button type="button" class="nav-link" aria-haspopup="true" aria-expanded="false" aria-controls={`nav-drop-${i}`}> |
| 70 | + {group.label} |
| 71 | + <svg viewBox="0 0 16 16" width="10" height="10" fill="none" aria-hidden="true"> |
| 72 | + <path d="M3.5 6l4.5 4.5L12.5 6" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" /> |
| 73 | + </svg> |
| 74 | + </button> |
| 75 | + <div class="nav-drop" id={`nav-drop-${i}`}> |
| 76 | + <ul> |
| 77 | + {group.links.map((link) => ( |
| 78 | + <li> |
| 79 | + <a href={link.href}>{link.label}</a> |
| 80 | + </li> |
| 81 | + ))} |
| 82 | + </ul> |
| 83 | + </div> |
| 84 | + </li> |
| 85 | + )) |
| 86 | + } |
| 87 | + <li class="nav-item"> |
| 88 | + <a class="nav-link" href={contributing.href}>{contributing.label}</a> |
| 89 | + </li> |
| 90 | + </ul> |
| 91 | + </nav> |
| 92 | + <div class="sl-flex print:hidden search-wrapper"> |
| 93 | + <Search /> |
| 94 | + </div> |
| 95 | + <div class="sl-hidden md:sl-flex print:hidden right-group"> |
| 96 | + <div class="sl-flex social-icons"> |
| 97 | + <SocialIcons /> |
| 98 | + </div> |
| 99 | + <ThemeSelect /> |
| 100 | + <LanguageSelect /> |
| 101 | + </div> |
| 102 | +</div> |
| 103 | + |
| 104 | +<style> |
| 105 | + .header { |
| 106 | + display: flex; |
| 107 | + gap: var(--sl-nav-gap); |
| 108 | + justify-content: space-between; |
| 109 | + align-items: center; |
| 110 | + height: 100%; |
| 111 | + /* floating glass pill, detached from the viewport edges */ |
| 112 | + border-radius: var(--ocx-radius-pill, 999px); |
| 113 | + border: 1px solid var(--sl-color-hairline-light); |
| 114 | + background-color: var(--ocx-glass-rail, canvas); |
| 115 | + backdrop-filter: var(--ocx-glass-blur, blur(22px)); |
| 116 | + -webkit-backdrop-filter: var(--ocx-glass-blur, blur(22px)); |
| 117 | + box-shadow: 0 4px 24px rgb(0 0 0 / 0.07); |
| 118 | + padding-inline: 1rem; |
| 119 | + } |
| 120 | + |
| 121 | + .title-wrapper { |
| 122 | + overflow: clip; |
| 123 | + padding: 0.25rem; |
| 124 | + margin: -0.25rem; |
| 125 | + min-width: 0; |
| 126 | + } |
| 127 | + |
| 128 | + /* below the sidebar breakpoint the fixed hamburger overlays the pill's right |
| 129 | + end — reserve room so search and the menu button never collide */ |
| 130 | + @media (max-width: 49.99rem) { |
| 131 | + :global([data-has-sidebar]) .header { |
| 132 | + padding-inline-end: calc(var(--sl-menu-button-size) + 0.75rem); |
| 133 | + } |
| 134 | + } |
| 135 | + |
| 136 | + .right-group, |
| 137 | + .social-icons { |
| 138 | + gap: 1rem; |
| 139 | + align-items: center; |
| 140 | + } |
| 141 | + .social-icons::after { |
| 142 | + content: ''; |
| 143 | + height: 2rem; |
| 144 | + border-inline-end: 1px solid var(--sl-color-gray-5); |
| 145 | + } |
| 146 | + |
| 147 | + /* ---- hover-dropdown nav (desktop only) ---- */ |
| 148 | + .glass-nav { |
| 149 | + display: none; |
| 150 | + } |
| 151 | + |
| 152 | + @media (min-width: 72rem) { |
| 153 | + .glass-nav { |
| 154 | + display: block; |
| 155 | + } |
| 156 | + .glass-nav > ul { |
| 157 | + display: flex; |
| 158 | + align-items: center; |
| 159 | + gap: 0.125rem; |
| 160 | + list-style: none; |
| 161 | + margin: 0; |
| 162 | + padding: 0; |
| 163 | + } |
| 164 | + .nav-item { |
| 165 | + position: relative; |
| 166 | + } |
| 167 | + .nav-link { |
| 168 | + display: inline-flex; |
| 169 | + align-items: center; |
| 170 | + gap: 0.375rem; |
| 171 | + padding: 0.375rem 0.875rem; |
| 172 | + border: 0; |
| 173 | + border-radius: var(--ocx-radius-pill, 999px); |
| 174 | + background: transparent; |
| 175 | + color: var(--sl-color-gray-2); |
| 176 | + font: inherit; |
| 177 | + font-size: var(--sl-text-sm); |
| 178 | + font-weight: 500; |
| 179 | + line-height: 1; |
| 180 | + text-decoration: none; |
| 181 | + cursor: pointer; |
| 182 | + white-space: nowrap; |
| 183 | + transition: background-color 0.12s, color 0.12s; |
| 184 | + } |
| 185 | + .nav-link svg { opacity: 0.55; transition: transform 0.15s; } |
| 186 | + .nav-item:hover .nav-link, |
| 187 | + .nav-item:focus-within .nav-link { |
| 188 | + background-color: var(--ocx-hover, rgba(128, 128, 128, 0.1)); |
| 189 | + color: var(--sl-color-white); |
| 190 | + } |
| 191 | + .nav-item:hover .nav-link svg, |
| 192 | + .nav-item:focus-within .nav-link svg { transform: rotate(180deg); } |
| 193 | + |
| 194 | + .nav-drop { |
| 195 | + position: absolute; |
| 196 | + top: 100%; |
| 197 | + left: 50%; |
| 198 | + translate: -50% 0; |
| 199 | + padding-top: 0.625rem; |
| 200 | + opacity: 0; |
| 201 | + visibility: hidden; |
| 202 | + transition: opacity 0.16s ease, visibility 0.16s, translate 0.16s ease; |
| 203 | + z-index: var(--sl-z-index-navbar); |
| 204 | + } |
| 205 | + .nav-item:hover .nav-drop, |
| 206 | + .nav-item:focus-within .nav-drop, |
| 207 | + .nav-item:has(> .nav-link[aria-expanded='true']) .nav-drop { |
| 208 | + opacity: 1; |
| 209 | + visibility: visible; |
| 210 | + translate: -50% 0.25rem; |
| 211 | + } |
| 212 | + .nav-drop > ul { |
| 213 | + list-style: none; |
| 214 | + margin: 0; |
| 215 | + padding: 0.375rem; |
| 216 | + min-width: 15rem; |
| 217 | + border: 1px solid var(--sl-color-gray-5); |
| 218 | + border-radius: var(--ocx-radius, 12px); |
| 219 | + background-color: var(--ocx-glass-panel, canvas); |
| 220 | + backdrop-filter: var(--ocx-glass-blur, blur(22px)); |
| 221 | + -webkit-backdrop-filter: var(--ocx-glass-blur, blur(22px)); |
| 222 | + box-shadow: var(--ocx-shadow, 0 10px 28px rgba(0, 0, 0, 0.2)); |
| 223 | + } |
| 224 | + .nav-drop a { |
| 225 | + display: block; |
| 226 | + padding: 0.5rem 0.75rem; |
| 227 | + border-radius: var(--ocx-radius-sm, 8px); |
| 228 | + color: var(--sl-color-gray-2); |
| 229 | + font-size: var(--sl-text-sm); |
| 230 | + text-decoration: none; |
| 231 | + transition: background-color 0.12s, color 0.12s; |
| 232 | + } |
| 233 | + .nav-drop a:hover, |
| 234 | + .nav-drop a:focus-visible { |
| 235 | + background-color: var(--ocx-hover, rgba(128, 128, 128, 0.1)); |
| 236 | + color: var(--sl-color-white); |
| 237 | + } |
| 238 | + } |
| 239 | + |
| 240 | + @media (prefers-reduced-motion: reduce) { |
| 241 | + .nav-link svg, |
| 242 | + .nav-drop { |
| 243 | + transition: none; |
| 244 | + } |
| 245 | + } |
| 246 | +</style> |
| 247 | + |
| 248 | +<script> |
| 249 | + // Click/Enter/Space toggles the dropdowns (hover and focus-within still work |
| 250 | + // via CSS); Escape or an outside click closes any open menu. |
| 251 | + const buttons = Array.from(document.querySelectorAll<HTMLButtonElement>('.glass-nav .nav-link[aria-haspopup]')); |
| 252 | + const closeAll = (except?: HTMLButtonElement) => { |
| 253 | + for (const b of buttons) if (b !== except) b.setAttribute('aria-expanded', 'false'); |
| 254 | + }; |
| 255 | + for (const btn of buttons) { |
| 256 | + btn.addEventListener('click', () => { |
| 257 | + const open = btn.getAttribute('aria-expanded') === 'true'; |
| 258 | + closeAll(btn); |
| 259 | + btn.setAttribute('aria-expanded', String(!open)); |
| 260 | + }); |
| 261 | + } |
| 262 | + document.addEventListener('click', (e) => { |
| 263 | + if (!(e.target instanceof Element) || !e.target.closest('.glass-nav')) closeAll(); |
| 264 | + }); |
| 265 | + document.addEventListener('keydown', (e) => { |
| 266 | + if (e.key === 'Escape') closeAll(); |
| 267 | + }); |
| 268 | +</script> |
0 commit comments