1---
2import Search from "@astrojs/starlight/components/Search.astro";
3import Logo from "./Logo.astro";
4
5interface 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
17const { 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>