-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathWarpTopbar.astro
More file actions
367 lines (360 loc) · 13.8 KB
/
WarpTopbar.astro
File metadata and controls
367 lines (360 loc) · 13.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
---
/**
* Slim Warp brand topbar for standalone pages that don't sit inside Starlight
* (currently just `/api`). Renders the Warp wordmark + docs breadcrumb + a
* small nav so the page reads as part of the same surface as the docs.
*
* The host page must reserve `--warp-topbar-height` (3.5rem by default) at
* the top of the viewport — see `src/pages/api.astro` where the same value
* feeds Scalar's `--scalar-y-offset` so its sticky sidebar clears the bar.
*
* Light/dark logo swap is keyed off the host's `.light-mode`/`.dark-mode`
* class on `<body>` (Scalar pattern). If you reuse this on a Starlight-
* themed page, wire similar classes or generalise the toggle.
*/
import logoDark from '../assets/warp-logo-dark.svg?raw';
import logoLight from '../assets/warp-logo-light.svg?raw';
interface NavLink {
href: string;
label: string;
/** Hide on narrow screens (≤ 640px). */
hideOnNarrow?: boolean;
external?: boolean;
}
interface Props {
/** Breadcrumb text after the wordmark (e.g. "API Reference"). */
crumb: string;
/** Right-side nav links. Defaults match the /api page. */
links?: NavLink[];
}
const {
crumb,
// No "active" treatment here — every link in this topbar navigates back
// into the Starlight docs, so there's no current-page surface to
// highlight. The topbar exists to anchor `/api` visually to the rest of
// the docs, not to indicate location within a tab set.
links = [
{ href: '/reference/api-and-sdk/', label: 'API & SDK', hideOnNarrow: true },
{ href: '/reference/api-and-sdk/quickstart/', label: 'Quickstart', hideOnNarrow: true },
],
} = Astro.props;
---
<header class="warp-topbar" role="banner">
<div class="warp-topbar__left">
{/* Wordmark links to warp.dev (matches CustomSiteTitle.astro on Starlight
pages). Users get to docs root via the adjacent "← Docs" breadcrumb. */}
<a href="https://warp.dev" class="warp-topbar__home" aria-label="Visit warp.dev">
<span class="warp-topbar__logo warp-topbar__logo--dark" aria-hidden="true" set:html={logoDark} />
<span class="warp-topbar__logo warp-topbar__logo--light" aria-hidden="true" set:html={logoLight} />
</a>
<a href="/" class="warp-topbar__docs">
<span>Docs</span>
</a>
<span class="warp-topbar__sep" aria-hidden="true">/</span>
<span class="warp-topbar__crumb">{crumb}</span>
</div>
<div class="warp-topbar__right">
<nav class="warp-topbar__nav" aria-label="Site">
{links.map((link) => (
<a
href={link.href}
class:list={['warp-topbar__link', link.hideOnNarrow && 'warp-topbar__link--hide-narrow']}
target={link.external ? '_blank' : undefined}
rel={link.external ? 'noopener' : undefined}
>
{link.label}
</a>
))}
</nav>
{/* Theme select — mirrors Starlight's ThemeSelect (Auto / Light / Dark)
so the picker lives in the same top-right slot as on the docs side
and shares `localStorage['starlight-theme']` for cross-page persistence.
Native `<select>` is positioned over the visible chip with `opacity:0`
so we get keyboard semantics + native dropdown UI for free. */}
<div class="warp-topbar__theme" data-warp-theme>
<span class="warp-topbar__theme-icon" aria-hidden="true" data-warp-theme-icon>
{/* Default icon paints the auto/laptop glyph; the inline script
below swaps it to sun/moon when the stored value is light/dark. */}
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="4" width="20" height="13" rx="2"/>
<path d="M2 21h20"/>
</svg>
</span>
<svg class="warp-topbar__theme-caret" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="m6 9 6 6 6-6"/>
</svg>
<select aria-label="Theme" data-warp-theme-select>
<option value="auto">Auto</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</div>
</div>
</header>
<style>
/* A slim brand bar that mirrors the Starlight site nav so standalone pages
feel like part of the same surface. The matching `--warp-topbar-height`
token in the host page's CSS feeds `--scalar-y-offset` (or equivalent)
so any sticky content clears the bar.
Default is 3.5rem (matches Starlight's `--sl-nav-height` mobile value);
the host page bumps the token to 4rem at >=50em so the bar lines up
with Starlight's desktop nav and the logo doesn't jump vertically when
you navigate from /docs to /api. */
.warp-topbar {
position: sticky;
top: 0;
z-index: 50;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
height: var(--warp-topbar-height, 3.5rem);
padding: 0 1rem;
background: var(--scalar-background-1, #121212);
border-bottom: 1px solid var(--scalar-border-color, rgba(255, 255, 255, 0.08));
font-family: var(--scalar-font, 'Inter', sans-serif);
color: var(--scalar-color-1, #fafafa);
}
.warp-topbar__left {
display: inline-flex;
align-items: center;
gap: 0.625rem;
min-width: 0;
}
.warp-topbar__home {
display: inline-flex;
align-items: center;
text-decoration: none;
color: inherit;
padding: 0.25rem 0.375rem;
margin-left: -0.375rem;
border-radius: var(--sl-radius-md);
}
.warp-topbar__home:hover { background: var(--scalar-background-2); }
.warp-topbar__home:focus-visible,
.warp-topbar__docs:focus-visible {
outline: 2px solid var(--scalar-color-accent);
outline-offset: 2px;
}
.warp-topbar__logo {
display: inline-flex;
align-items: center;
flex-shrink: 0;
}
.warp-topbar__logo :global(svg),
.warp-topbar__logo > svg {
height: 1.25rem;
width: auto;
max-width: 100%;
display: block;
}
/* Light/dark logo swap, keyed off the host's .light-mode/.dark-mode class. */
.warp-topbar__logo--dark { display: none; }
body.dark-mode .warp-topbar__logo--dark { display: inline-flex; }
body.dark-mode .warp-topbar__logo--light { display: none; }
.warp-topbar__sep {
color: var(--scalar-color-3);
opacity: 0.6;
font-weight: 300;
font-size: 1rem;
}
.warp-topbar__crumb {
color: var(--scalar-color-2);
font-size: 0.875rem;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.warp-topbar__docs {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0 0.125rem;
text-decoration: none;
color: var(--scalar-color-3);
font-size: 0.8125rem;
font-weight: 500;
line-height: 1.25;
transition: color 0.15s ease;
white-space: nowrap;
border-radius: var(--sl-radius-xs);
}
.warp-topbar__docs:hover {
color: var(--scalar-color-1);
}
/* Right group wraps the nav and the theme select so the topbar's
`justify-content: space-between` keeps the home/crumb pinned left and
the whole right cluster pinned right. */
.warp-topbar__right {
display: inline-flex;
align-items: center;
gap: 1rem;
}
/* Right-side nav: every link is an outbound jump to the Starlight docs,
so there's no "active" tab to indicate. We keep the calm gray-3 idle
→ full-emphasis white hover progression from the topic nav, but drop
the accent underline + active treatment — nothing here is the
current page. */
.warp-topbar__nav {
display: inline-flex;
align-items: center;
gap: 1.25rem;
}
.warp-topbar__link {
/* Flat type, no chip surface. `--scalar-color-3` / `-color-1` are the
Scalar equivalents of `--sl-color-gray-3` / `-color-white`, mapped
to Warp brand tokens in `pages/api.astro`. Visual weight lives
entirely in the type color shift on hover. */
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0 0.125rem;
text-decoration: none;
color: var(--scalar-color-3);
font-size: 0.8125rem;
font-weight: 500;
line-height: 1.25;
transition: color 0.15s ease;
white-space: nowrap;
}
.warp-topbar__link:hover {
color: var(--scalar-color-1);
}
.warp-topbar__link:focus-visible {
outline: 2px solid var(--scalar-color-accent);
outline-offset: 2px;
border-radius: var(--sl-radius-xs);
}
/* Theme select chip. The visible chrome is `[icon] [caret]`; the actual
`<select>` sits on top with `opacity: 0` so clicks/keyboard open the
native dropdown but the chip carries the styling. We rely on `:has()`
for keyboard-only focus (modern browsers, gracefully degrades to no
focus ring on older ones — the underlying select still gets the
native keyboard outline if the chip's outline doesn't apply). */
.warp-topbar__theme {
position: relative;
display: inline-flex;
align-items: center;
gap: 0.25rem;
height: 2rem;
padding: 0 0.375rem;
border-radius: var(--sl-radius-sm);
color: var(--scalar-color-3);
cursor: pointer;
transition: color 0.15s ease, background-color 0.15s ease;
}
.warp-topbar__theme:hover {
color: var(--scalar-color-1);
background: var(--scalar-background-2);
}
.warp-topbar__theme:has(select:focus-visible) {
outline: 2px solid var(--scalar-color-accent);
outline-offset: 2px;
}
.warp-topbar__theme-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 14px;
height: 14px;
}
.warp-topbar__theme-icon :global(svg) {
width: 14px;
height: 14px;
display: block;
}
.warp-topbar__theme-caret {
display: inline-flex;
opacity: 0.7;
}
.warp-topbar__theme select {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
border: 0;
background: transparent;
color: inherit;
font: inherit;
opacity: 0;
cursor: pointer;
appearance: none;
}
.warp-topbar__theme select:focus {
outline: none;
}
@media (max-width: 640px) {
.warp-topbar { padding: 0 0.75rem; }
.warp-topbar__sep,
.warp-topbar__crumb { display: none; }
.warp-topbar__link--hide-narrow { display: none; }
}
</style>
<script is:inline>
// Theme select wiring (drives `localStorage['starlight-theme']` so it
// round-trips with Starlight's own ThemeSelect on the docs side). The
// heavy lifting — resolving auto, applying body class, syncing across
// tabs and `prefers-color-scheme` changes — lives in the inline init
// script in `pages/api.astro`, exposed as `window.__warpApplyTheme`.
// This script just wires the select UI up to that helper and keeps the
// visible icon in sync with the stored value.
(function () {
var root = document.querySelector('[data-warp-theme]');
if (!root) return;
var select = root.querySelector('[data-warp-theme-select]');
var iconWrap = root.querySelector('[data-warp-theme-icon]');
if (!select || !iconWrap) return;
// Inline SVGs match the visual weight of Starlight's `sun`/`moon`/
// `laptop` icons in `node_modules/@astrojs/starlight/components-internals/Icons.ts`.
var ICONS = {
auto: '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="13" rx="2"/><path d="M2 21h20"/></svg>',
light: '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path 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"/></svg>',
dark: '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>',
};
var read = function () {
try {
var v = localStorage.getItem('starlight-theme');
return v === 'light' || v === 'dark' ? v : 'auto';
} catch (_e) { return 'auto'; }
};
var setIcon = function (v) {
iconWrap.innerHTML = ICONS[v] || ICONS.auto;
};
var setValue = function (v) {
select.value = v;
setIcon(v);
};
setValue(read());
select.addEventListener('change', function () {
var v = select.value;
setIcon(v);
if (typeof window.__warpApplyTheme === 'function') {
window.__warpApplyTheme(v);
} else {
// Fallback: write to storage and apply minimally. Used only if the
// page-level helper isn't loaded (e.g. WarpTopbar reused outside
// /api in the future).
try {
localStorage.setItem('starlight-theme', v === 'auto' ? '' : v);
} catch (_e) {}
var resolved = v === 'auto'
? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
: v;
document.body.classList.toggle('dark-mode', resolved === 'dark');
document.body.classList.toggle('light-mode', resolved !== 'dark');
document.documentElement.setAttribute('data-theme', resolved);
}
});
// Cross-tab sync: another tab updated the preference — mirror it in
// our select UI. (The page-level script in api.astro handles
// re-applying the resolved theme; we just keep the icon honest here.)
window.addEventListener('storage', function (e) {
if (e.key !== 'starlight-theme') return;
var v = e.newValue;
setValue(v === 'light' || v === 'dark' ? v : 'auto');
});
})();
</script>