| 1 | --- |
| 2 | import Search from "@astrojs/starlight/components/Search.astro"; |
| 3 | import Logo from "./Logo.astro"; |
| 4 | |
| 5 | interface Props { |
| 6 | /** |
| 7 | * Set on the marketing layout, where this nav is the only header and there |
| 8 | * is no Starlight sidebar — so every control stays in the top bar on all |
| 9 | * viewports. Docs pages render this as Starlight's `Header` override and |
| 10 | * leave it unset; there the GitHub/Discord links are dropped on mobile (they |
| 11 | * live in the hamburger menu footer) while the theme toggle stays in the top |
| 12 | * bar (see the `.alc-nav--docs` mobile rule below). |
| 13 | */ |
| 14 | marketing?: boolean; |
| 15 | } |
| 16 | |
| 17 | const { marketing = false } = Astro.props; |
| 18 | --- |
| 19 | |
| 20 | <nav class:list={["alc-nav", { "alc-nav--docs": !marketing }]}> |
| 21 | <a href="/" class="alc-nav__brand"> |
| 22 | <Logo /> |
| 23 | </a> |
| 24 | |
| 25 | <div class="alc-nav__spacer"></div> |
| 26 | <div class="alc-nav__search"> |
| 27 | <Search /> |
| 28 | </div> |
| 29 | <a href="/getting-started" class="alc-nav__link alc-nav__link--docs">Docs</a> |
| 30 | <a href="/blog" class="alc-nav__link">Blog</a> |
| 31 | <a href="/compare" class="alc-nav__link">Compare</a> |
| 32 | <a |
| 33 | href="https://github.com/alchemy-run/alchemy" |
| 34 | class="alc-nav__link alc-nav__link--icon" |
| 35 | aria-label="GitHub" |
| 36 | > |
| 37 | <svg aria-hidden width="18" height="18" viewBox="0 0 24 24" fill="currentColor"> |
| 38 | <path |
| 39 | d="M12 .5C5.73.5.5 5.73.5 12c0 5.08 3.29 9.39 7.86 10.91.58.1.79-.25.79-.56 0-.28-.01-1.02-.02-2-3.2.69-3.88-1.54-3.88-1.54-.52-1.33-1.28-1.69-1.28-1.69-1.05-.72.08-.71.08-.71 1.16.08 1.77 1.19 1.77 1.19 1.03 1.77 2.71 1.26 3.37.96.1-.75.4-1.26.73-1.55-2.55-.29-5.24-1.28-5.24-5.69 0-1.26.45-2.29 1.19-3.1-.12-.29-.52-1.46.11-3.05 0 0 .97-.31 3.18 1.18a11.04 11.04 0 0 1 5.79 0c2.21-1.49 3.18-1.18 3.18-1.18.63 1.59.23 2.76.11 3.05.74.81 1.19 1.84 1.19 3.1 0 4.42-2.69 5.39-5.25 5.68.41.36.78 1.07.78 2.16 0 1.56-.01 2.81-.01 3.19 0 .31.21.67.8.56C20.71 21.39 24 17.08 24 12 24 5.73 18.27.5 12 .5z" |
| 40 | ></path> |
| 41 | </svg> |
| 42 | <span class="alc-nav__link-text">GitHub</span> |
| 43 | </a> |
| 44 | <a |
| 45 | href="/discord" |
| 46 | class="alc-nav__link alc-nav__link--icon" |
| 47 | aria-label="Discord" |
| 48 | > |
| 49 | <svg aria-hidden width="18" height="18" viewBox="0 0 24 24" fill="currentColor"> |
| 50 | <path |
| 51 | d="M20.317 4.369A19.79 19.79 0 0 0 16.558 3.2a.075.075 0 0 0-.079.037c-.34.6-.717 1.384-.98 2-1.797-.27-3.583-.27-5.342 0-.263-.63-.654-1.4-.997-2A.077.077 0 0 0 9.08 3.2a19.736 19.736 0 0 0-3.76 1.169.07.07 0 0 0-.032.027C1.533 9.046.6 13.58 1.057 18.058a.082.082 0 0 0 .031.056 19.9 19.9 0 0 0 5.993 3.029.077.077 0 0 0 .084-.027c.462-.63.873-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.108 13.108 0 0 1-1.872-.892.077.077 0 0 1-.008-.128c.126-.094.252-.192.372-.291a.074.074 0 0 1 .077-.01c3.927 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.099.246.197.373.291a.077.077 0 0 1-.006.128 12.3 12.3 0 0 1-1.873.892.077.077 0 0 0-.04.106c.36.699.772 1.364 1.225 1.994a.076.076 0 0 0 .084.028 19.84 19.84 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.548-13.66a.061.061 0 0 0-.031-.029ZM8.02 15.331c-1.183 0-2.157-1.085-2.157-2.42 0-1.333.956-2.418 2.157-2.418 1.21 0 2.176 1.094 2.157 2.418 0 1.335-.956 2.42-2.157 2.42Zm7.974 0c-1.183 0-2.157-1.085-2.157-2.42 0-1.333.955-2.418 2.157-2.418 1.21 0 2.176 1.094 2.157 2.418 0 1.335-.946 2.42-2.157 2.42Z" |
| 52 | ></path> |
| 53 | </svg> |
| 54 | <span class="alc-nav__link-text">Discord</span> |
| 55 | </a> |
| 56 | <button |
| 57 | type="button" |
| 58 | class="alc-nav__theme-toggle" |
| 59 | aria-label="Toggle color theme" |
| 60 | title="Toggle color theme" |
| 61 | data-theme-toggle |
| 62 | > |
| 63 | {/* Sun — shown in dark mode (click → light) */} |
| 64 | <svg |
| 65 | class="alc-theme-icon alc-theme-icon--sun" |
| 66 | aria-hidden="true" |
| 67 | width="18" |
| 68 | height="18" |
| 69 | viewBox="0 0 24 24" |
| 70 | fill="none" |
| 71 | stroke="currentColor" |
| 72 | stroke-width="1.75" |
| 73 | stroke-linecap="round" |
| 74 | stroke-linejoin="round" |
| 75 | > |
| 76 | <circle cx="12" cy="12" r="4"></circle> |
| 77 | <path |
| 78 | d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41" |
| 79 | ></path> |
| 80 | </svg> |
| 81 | {/* Moon — shown in light mode (click → dark) */} |
| 82 | <svg |
| 83 | class="alc-theme-icon alc-theme-icon--moon" |
| 84 | aria-hidden="true" |
| 85 | width="18" |
| 86 | height="18" |
| 87 | viewBox="0 0 24 24" |
| 88 | fill="none" |
| 89 | stroke="currentColor" |
| 90 | stroke-width="1.75" |
| 91 | stroke-linecap="round" |
| 92 | stroke-linejoin="round" |
| 93 | > |
| 94 | <path d="M21 12.79A9 9 0 1 1 11.21 3a7 7 0 0 0 9.79 9.79z"></path> |
| 95 | </svg> |
| 96 | </button> |
| 97 | </nav> |
| 98 | |
| 99 | <style> |
| 100 | .alc-nav, |
| 101 | .alc-nav * { |
| 102 | box-sizing: border-box; |
| 103 | } |
| 104 | .alc-nav { |
| 105 | height: 100%; |
| 106 | display: flex; |
| 107 | align-items: center; |
| 108 | gap: 24px; |
| 109 | } |
| 110 | /* Flex + fixed line-height so the logo centers the same way on marketing |
| 111 | pages (body line-height 1.6) and Starlight pages (1.75). */ |
| 112 | .alc-nav__brand { |
| 113 | display: flex; |
| 114 | align-items: center; |
| 115 | line-height: 1; |
| 116 | text-decoration: none; |
| 117 | } |
| 118 | .alc-nav__spacer { |
| 119 | flex: 1; |
| 120 | } |
| 121 | .alc-nav__search { |
| 122 | display: flex; |
| 123 | align-items: center; |
| 124 | width: 220px; |
| 125 | } |
| 126 | .alc-nav__search :global(site-search) { |
| 127 | display: block; |
| 128 | width: 100%; |
| 129 | } |
| 130 | .alc-nav__search :global(button[data-open-modal]) { |
| 131 | display: flex; |
| 132 | align-items: center; |
| 133 | gap: 8px; |
| 134 | width: 100%; |
| 135 | height: 32px; |
| 136 | padding: 0 8px 0 12px; |
| 137 | background: var(--alc-bg); |
| 138 | border: 1px solid var(--alc-hairline-2); |
| 139 | border-radius: 8px; |
| 140 | font-size: 12px; |
| 141 | color: var(--alc-fg-4); |
| 142 | cursor: pointer; |
| 143 | font-family: inherit; |
| 144 | } |
| 145 | .alc-nav__search :global(button[data-open-modal]:hover) { |
| 146 | border-color: var(--alc-fg-4); |
| 147 | color: var(--alc-fg-2); |
| 148 | } |
| 149 | .alc-nav__search :global(button[data-open-modal] svg) { |
| 150 | width: 14px; |
| 151 | height: 14px; |
| 152 | flex-shrink: 0; |
| 153 | } |
| 154 | .alc-nav__search :global(button[data-open-modal] > :last-child) { |
| 155 | margin-inline-start: auto; |
| 156 | } |
| 157 | .alc-nav__search :global(button[data-open-modal] kbd) { |
| 158 | display: inline-flex; |
| 159 | align-items: center; |
| 160 | gap: 2px; |
| 161 | background: transparent; |
| 162 | border: 0; |
| 163 | padding: 0; |
| 164 | font-family: var(--alc-font-mono); |
| 165 | font-size: 11px; |
| 166 | color: var(--alc-fg-2); |
| 167 | } |
| 168 | .alc-nav__search :global(button[data-open-modal] kbd kbd) { |
| 169 | background: var(--alc-bg-elev-1); |
| 170 | border: 1px solid var(--alc-hairline-2); |
| 171 | border-radius: 4px; |
| 172 | padding: 1px 5px; |
| 173 | } |
| 174 | .alc-nav__theme-toggle { |
| 175 | display: inline-flex; |
| 176 | align-items: center; |
| 177 | justify-content: center; |
| 178 | width: 32px; |
| 179 | height: 32px; |
| 180 | padding: 0; |
| 181 | border: 1px solid var(--alc-hairline-2); |
| 182 | border-radius: 8px; |
| 183 | background: transparent; |
| 184 | color: var(--alc-fg-2); |
| 185 | cursor: pointer; |
| 186 | font: inherit; |
| 187 | transition: |
| 188 | color var(--alc-dur) var(--alc-ease), |
| 189 | border-color var(--alc-dur) var(--alc-ease), |
| 190 | background-color var(--alc-dur) var(--alc-ease); |
| 191 | } |
| 192 | .alc-nav__theme-toggle:hover { |
| 193 | color: var(--alc-accent-deep); |
| 194 | border-color: var(--alc-accent-40); |
| 195 | background: var(--alc-bg-elev-1); |
| 196 | } |
| 197 | .alc-nav__theme-toggle:focus-visible { |
| 198 | outline: 2px solid var(--alc-accent-40); |
| 199 | outline-offset: 2px; |
| 200 | } |
| 201 | .alc-theme-icon { |
| 202 | display: block; |
| 203 | } |
| 204 | /* Show one icon per mode. Default (no [data-theme] yet, before pre-paint |
| 205 | script runs) shows the moon — matches the historical light bias. */ |
| 206 | .alc-theme-icon--sun { display: none; } |
| 207 | :global([data-theme="dark"]) .alc-theme-icon--sun { display: block; } |
| 208 | :global([data-theme="dark"]) .alc-theme-icon--moon { display: none; } |
| 209 | .alc-nav__link { |
| 210 | color: var(--alc-fg-2); |
| 211 | text-decoration: none; |
| 212 | font-size: 14px; |
| 213 | } |
| 214 | .alc-nav__link:hover { |
| 215 | color: var(--alc-accent-deep); |
| 216 | } |
| 217 | .alc-nav__link--icon { |
| 218 | display: inline-flex; |
| 219 | align-items: center; |
| 220 | gap: 6px; |
| 221 | } |
| 222 | |
| 223 | /* Collapse search to an icon-only button (matching the theme toggle) — |
| 224 | docs pages have no other search entry point, so it must stay reachable. */ |
| 225 | @media (max-width: 880px) { |
| 226 | .alc-nav__search { |
| 227 | width: auto; |
| 228 | } |
| 229 | .alc-nav__search :global(button[data-open-modal]) { |
| 230 | width: 32px; |
| 231 | justify-content: center; |
| 232 | padding: 0; |
| 233 | } |
| 234 | .alc-nav__search :global(button[data-open-modal] svg) { |
| 235 | width: 16px; |
| 236 | height: 16px; |
| 237 | } |
| 238 | .alc-nav__search :global(button[data-open-modal] > span), |
| 239 | .alc-nav__search :global(button[data-open-modal] kbd) { |
| 240 | display: none; |
| 241 | } |
| 242 | .alc-nav__link--icon .alc-nav__link-text { |
| 243 | display: none; |
| 244 | } |
| 245 | } |
| 246 | @media (max-width: 640px) { |
| 247 | .alc-nav { gap: 16px; } |
| 248 | } |
| 249 | @media (max-width: 480px) { |
| 250 | .alc-nav { gap: 12px; } |
| 251 | } |
| 252 | /* On phones, Docs, Blog, Compare and the icons don't all fit. Docs is |
| 253 | the one to drop: the hero CTA and the footer both lead there. */ |
| 254 | @media (max-width: 460px) { |
| 255 | .alc-nav { gap: 10px; } |
| 256 | .alc-nav__link--docs { display: none; } |
| 257 | } |
| 258 | @media (max-width: 380px) { |
| 259 | .alc-nav { gap: 6px; } |
| 260 | /* The wordmark's size is an inline style (Logo's size prop). */ |
| 261 | .alc-nav__brand :global(.alc-logo__word) { font-size: 19px !important; } |
| 262 | } |
| 263 | /* |
| 264 | * Docs pages render this nav inside Starlight's fixed `<header>`, which |
| 265 | * reserves room on the right for the floating hamburger button and shows a |
| 266 | * mobile menu (with theme select + social icons in its footer) below the |
| 267 | * `md` (50rem) breakpoint. Packing every control into the top bar too made it |
| 268 | * overflow past that reserved area and slide under the hamburger. Below 50rem |
| 269 | * we drop just the GitHub/Discord links — they live in the hamburger menu — |
| 270 | * but keep the theme toggle in the top bar so it stays one tap away. The |
| 271 | * docs gutter is also trimmed on mobile (see `--sl-nav-pad-x` in custom.css) |
| 272 | * to make room. Marketing pages have no hamburger/menu, so they keep every |
| 273 | * control (see `marketing`). |
| 274 | */ |
| 275 | @media (max-width: 50rem) { |
| 276 | .alc-nav--docs .alc-nav__link--icon { |
| 277 | display: none; |
| 278 | } |
| 279 | } |
| 280 | </style> |
| 281 | |
| 282 | <style is:global> |
| 283 | /* |
| 284 | * Starlight's Search dialog is rendered in the top layer (outside any |
| 285 | * scoped style boundary) and relies on Starlight tokens / utility classes |
| 286 | * (`--sl-color-*`, `sl-hidden`, `md:sl-hidden`) that are not loaded on |
| 287 | * marketing pages. We map the missing Starlight color tokens onto our |
| 288 | * parchment palette inside the search dialog so Starlight's own Pagefind |
| 289 | * styling (page icon, sub-result tree connector, focus rings, etc.) |
| 290 | * "just works" on every page. |
| 291 | */ |
| 292 | site-search { |
| 293 | /* Starlight color tokens, mapped to our palette. Scoped to site-search |
| 294 | so docs pages keep their existing Starlight theme untouched. */ |
| 295 | --sl-color-white: var(--alc-fg-1); |
| 296 | --sl-color-text: var(--alc-fg-1); |
| 297 | --sl-color-text-accent: var(--alc-accent-deep); |
| 298 | --sl-color-accent: var(--alc-accent); |
| 299 | --sl-color-accent-high: var(--alc-accent-deep); |
| 300 | --sl-color-accent-low: var(--alc-accent-12); |
| 301 | --sl-color-gray-1: var(--alc-fg-1); |
| 302 | --sl-color-gray-2: var(--alc-fg-2); |
| 303 | --sl-color-gray-3: var(--alc-fg-3); |
| 304 | --sl-color-gray-4: var(--alc-fg-4); |
| 305 | --sl-color-gray-5: var(--alc-hairline-2); |
| 306 | --sl-color-gray-6: var(--alc-bg-sunk); |
| 307 | --sl-color-black: var(--alc-bg-elev-1); |
| 308 | --sl-color-backdrop-overlay: rgba(42, 38, 32, 0.35); |
| 309 | --sl-shadow-lg: 0 24px 64px rgba(42, 38, 32, 0.25); |
| 310 | --__sl-font: var(--alc-font-sans); |
| 311 | } |
| 312 | |
| 313 | site-search dialog { |
| 314 | color: var(--alc-fg-2); |
| 315 | border-radius: 12px; |
| 316 | font-family: var(--alc-font-sans); |
| 317 | } |
| 318 | site-search dialog::backdrop { |
| 319 | -webkit-backdrop-filter: blur(6px); |
| 320 | backdrop-filter: blur(6px); |
| 321 | } |
| 322 | |
| 323 | /* Hide the mobile-only Cancel button on desktop. Starlight uses |
| 324 | `md:sl-hidden` for this but the utility CSS isn't loaded on the |
| 325 | marketing pages, so it otherwise leaks into the input on desktop. */ |
| 326 | site-search button[data-close-modal] { |
| 327 | display: none; |
| 328 | font-family: var(--alc-font-sans); |
| 329 | } |
| 330 | @media (max-width: 50rem) { |
| 331 | site-search button[data-close-modal] { |
| 332 | display: inline-flex; |
| 333 | /* Starlight positions this button with `position: absolute` + |
| 334 | `align-self: flex-end`, relying on the static position of an |
| 335 | abs-pos flex child honoring alignment. WebKit doesn't implement |
| 336 | that, so on iOS the button lands at the frame's START and |
| 337 | overlaps the search input's magnifier/text. Pin it explicitly: |
| 338 | 1rem matches .dialog-frame's mobile padding, which is exactly |
| 339 | where flex-end places it in browsers that do honor alignment — |
| 340 | inside the 5rem gutter that --sl-search-cancel-space reserves |
| 341 | to the right of the input. */ |
| 342 | inset-inline-end: 1rem; |
| 343 | } |
| 344 | } |
| 345 | |
| 346 | /* ── Pagefind UI re-skin (Effect-style result cards) ────────────────── */ |
| 347 | site-search #starlight__search { |
| 348 | --pagefind-ui-primary: var(--alc-fg-1); |
| 349 | --pagefind-ui-text: var(--alc-fg-2); |
| 350 | --pagefind-ui-background: var(--alc-bg-sunk); |
| 351 | --pagefind-ui-border: var(--alc-hairline-2); |
| 352 | --pagefind-ui-tag: var(--alc-bg-sunk); |
| 353 | --pagefind-ui-border-width: 1px; |
| 354 | --pagefind-ui-font: var(--alc-font-sans); |
| 355 | --pagefind-ui-scale: 0.85; |
| 356 | } |
| 357 | /* Search input — sits on a slightly sunk surface so the white result |
| 358 | cards beneath it have something to pop against. */ |
| 359 | site-search #starlight__search .pagefind-ui__form { |
| 360 | border-bottom: 1px solid var(--alc-hairline-2); |
| 361 | padding-bottom: 0.75rem; |
| 362 | margin-bottom: 0.25rem; |
| 363 | } |
| 364 | site-search #starlight__search .pagefind-ui__search-input { |
| 365 | color: var(--alc-fg-1); |
| 366 | background: var(--alc-bg-elev-1); |
| 367 | font-family: var(--alc-font-sans); |
| 368 | border: 1px solid var(--alc-hairline-2); |
| 369 | border-radius: 8px; |
| 370 | } |
| 371 | site-search #starlight__search input:focus { |
| 372 | --pagefind-ui-border: var(--alc-accent); |
| 373 | box-shadow: 0 0 0 3px var(--alc-accent-12); |
| 374 | } |
| 375 | site-search #starlight__search .pagefind-ui__message { |
| 376 | color: var(--alc-fg-3); |
| 377 | font-weight: 500; |
| 378 | padding-block: 0.5rem; |
| 379 | } |
| 380 | |
| 381 | /* Each "result" = a page = a white elevated card. */ |
| 382 | site-search #starlight__search .pagefind-ui__results { |
| 383 | display: flex; |
| 384 | flex-direction: column; |
| 385 | gap: 12px; |
| 386 | } |
| 387 | site-search #starlight__search .pagefind-ui__results > * + * { |
| 388 | margin-top: 0; |
| 389 | } |
| 390 | site-search #starlight__search .pagefind-ui__result { |
| 391 | background: var(--alc-bg-elev-1); |
| 392 | border: 1px solid var(--alc-hairline); |
| 393 | border-radius: 10px; |
| 394 | box-shadow: 0 1px 2px rgba(42, 38, 32, 0.04); |
| 395 | overflow: hidden; |
| 396 | } |
| 397 | site-search #starlight__search .pagefind-ui__result-inner { |
| 398 | background: transparent; |
| 399 | } |
| 400 | /* Top-level page row + sub-result rows share the same elevated card, |
| 401 | so we drop the per-row backgrounds Starlight applies. */ |
| 402 | site-search |
| 403 | #starlight__search |
| 404 | .pagefind-ui__result-title:not(:where(.pagefind-ui__result-nested *)), |
| 405 | site-search #starlight__search .pagefind-ui__result-nested { |
| 406 | background: transparent; |
| 407 | } |
| 408 | /* Hover/focus highlight — soft accent tint instead of a hard outline. */ |
| 409 | site-search |
| 410 | #starlight__search |
| 411 | .pagefind-ui__result-title:not(:where(.pagefind-ui__result-nested *)):hover, |
| 412 | site-search #starlight__search .pagefind-ui__result-nested:hover, |
| 413 | site-search |
| 414 | #starlight__search |
| 415 | .pagefind-ui__result-title:not( |
| 416 | :where(.pagefind-ui__result-nested *) |
| 417 | ):focus-within, |
| 418 | site-search #starlight__search .pagefind-ui__result-nested:focus-within { |
| 419 | outline: none; |
| 420 | background: var(--alc-accent-12); |
| 421 | } |
| 422 | |
| 423 | site-search #starlight__search .pagefind-ui__result-link { |
| 424 | --pagefind-ui-text: var(--alc-fg-1); |
| 425 | color: var(--alc-fg-1); |
| 426 | font-weight: 700; |
| 427 | font-size: 1rem; |
| 428 | } |
| 429 | site-search #starlight__search .pagefind-ui__result-excerpt { |
| 430 | color: var(--alc-fg-3); |
| 431 | font-size: 0.9rem; |
| 432 | line-height: 1.55; |
| 433 | margin-top: 0.25rem; |
| 434 | } |
| 435 | site-search #starlight__search mark { |
| 436 | color: var(--alc-accent-deep); |
| 437 | background: transparent; |
| 438 | font-weight: 700; |
| 439 | } |
| 440 | /* Page / tree icons recolored to fit parchment. */ |
| 441 | site-search #starlight__search .pagefind-ui__result-title::before { |
| 442 | background: var(--alc-fg-4) !important; |
| 443 | } |
| 444 | site-search #starlight__search .pagefind-ui__result-nested::before { |
| 445 | background: var(--alc-hairline-3) !important; |
| 446 | } |
| 447 | </style> |
| 448 | |
| 449 | <script is:inline> |
| 450 | (() => { |
| 451 | const button = document.querySelector("[data-theme-toggle]"); |
| 452 | if (!button) return; |
| 453 | const STORAGE_KEY = "starlight-theme"; |
| 454 | button.addEventListener("click", () => { |
| 455 | const current = document.documentElement.dataset.theme === "dark" |
| 456 | ? "dark" |
| 457 | : "light"; |
| 458 | const next = current === "dark" ? "light" : "dark"; |
| 459 | document.documentElement.dataset.theme = next; |
| 460 | try { |
| 461 | localStorage.setItem(STORAGE_KEY, next); |
| 462 | } catch {} |
| 463 | }); |
| 464 | })(); |
| 465 | </script> |