Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
197 linesCodeBlameRaw
1---
2// The docs header: the wordmark, search in the middle and links on the
3// right, with the tabs that divide the docs beneath it.
4//
5// Its own layout, not Starlight's: Starlight lines the search up with the
6// page's text column, which moves when a page has an "On this page" list
7// and when it does not. Here the row is three columns, the outer two always
8// the same width, so the search sits at the header's centre on every page.
9import Search from '@astrojs/starlight/components/Search.astro';
10
11import { NAV } from '../route-data';
12import SiteTitle from './SiteTitle.astro';
13import SocialIcons from './SocialIcons.astro';
14// Auto, Light or Dark, kept in this browser; Auto follows the system.
15// On tablets and phones it is at the foot of the menu instead.
16import ThemeToggle from './ThemeToggle.astro';
17
18const tabs = NAV.get(Astro.locals.starlightRoute)?.tabs ?? [];
19---
20
21<div class="g1t-header">
22 <div class="g1t-header-top">
23 <div class="g1t-header-title"><SiteTitle /></div>
24 <div class="g1t-header-search print:hidden"><Search /></div>
25 <div class="g1t-header-links print:hidden">
26 <SocialIcons />
27 <div class="g1t-header-theme"><ThemeToggle /></div>
28 </div>
29 </div>
30 <nav class="g1t-tabs" aria-label="Sections">
31 {
32 tabs.map((tab) => (
33 <a href={tab.href} aria-current={tab.current ? 'page' : undefined}>
34 {tab.label}
35 </a>
36 ))
37 }
38 </nav>
39</div>
40
41<script>
42 // On a phone the tabs scroll sideways: start with the current one in view.
43 const current = document.querySelector<HTMLElement>('.g1t-tabs [aria-current]');
44 const tabs = current?.parentElement;
45 if (current && tabs && current.offsetLeft + current.offsetWidth > tabs.clientWidth) {
46 tabs.scrollLeft = current.offsetLeft - (tabs.clientWidth - current.offsetWidth) / 2;
47 }
48 // The row fades out at an edge with more tabs past it, so a cut-off
49 // tab reads as "scroll for more" rather than as a clipped word.
50 const bar = document.querySelector<HTMLElement>('.g1t-tabs');
51 if (bar) {
52 const edges = () => {
53 const end = bar.scrollWidth - bar.clientWidth;
54 bar.toggleAttribute('data-more-start', bar.scrollLeft > 1);
55 bar.toggleAttribute('data-more-end', bar.scrollLeft < end - 1);
56 };
57 edges();
58 bar.addEventListener('scroll', edges, { passive: true });
59 addEventListener('resize', edges);
60 }
61</script>
62
63<style>
64 .g1t-header {
65 display: flex;
66 flex-direction: column;
67 height: 100%;
68 }
69 /* The same bar as the app's: 3.5rem, and a line under it. */
70 .g1t-header-top {
71 /* Each side column is at least as wide as the widest thing that goes
72 in either, so the two stay equal and the search stays centred. */
73 --g1t-header-side: 13.5rem;
74 box-sizing: border-box;
75 display: grid;
76 grid-template-columns:
77 minmax(var(--g1t-header-side), 1fr)
78 minmax(0, 26rem)
79 minmax(var(--g1t-header-side), 1fr);
80 align-items: center;
81 gap: 1rem;
82 height: 3.5rem;
83 flex-shrink: 0;
84 padding: 0 var(--sl-nav-pad-x);
85 border-bottom: 1px solid var(--g1t-line);
86 }
87 .g1t-header-title {
88 display: flex;
89 min-width: 0;
90 /* Room for the link's focus ring. */
91 padding: 0.25rem;
92 margin: -0.25rem;
93 }
94 .g1t-header-search {
95 display: flex;
96 justify-content: center;
97 min-width: 0;
98 }
99 .g1t-header-links {
100 display: flex;
101 align-items: center;
102 justify-content: flex-end;
103 gap: 0.5rem;
104 min-width: 0;
105 }
106 .g1t-header-theme {
107 display: flex;
108 align-items: center;
109 }
110 /* Desktop: the links and the theme button together are wider. */
111 @media (min-width: 50rem) {
112 .g1t-header-top {
113 --g1t-header-side: 17.5rem;
114 }
115 }
116 @media (min-width: 64rem) {
117 .g1t-header-top {
118 --g1t-header-side: 21rem;
119 }
120 }
121 /* Tablets and phones: the theme select is at the foot of the menu. */
122 @media (max-width: 49.99rem) {
123 .g1t-header-theme {
124 display: none;
125 }
126 }
127 /* Tablets: the sidebar is a menu, and its button (fixed in place by
128 Starlight) takes the row's end. The right column makes room for it
129 inside itself, so the two columns stay equal and the search centred. */
130 @media (max-width: 49.99rem) {
131 .g1t-header-top {
132 --g1t-header-side: 13rem;
133 }
134 :global([data-has-sidebar]) .g1t-header-links {
135 padding-inline-end: calc(var(--sl-menu-button-size) + 0.5rem);
136 }
137 }
138 /* Phones: the wordmark on the left; search, the links and the menu
139 button together on the right. */
140 @media (max-width: 44.99rem) {
141 .g1t-header-top {
142 grid-template-columns: minmax(0, 1fr) auto auto;
143 gap: 0.25rem;
144 }
145 }
146 .g1t-tabs {
147 /* The tabs' own offsets are measured from here. */
148 position: relative;
149 display: flex;
150 gap: 1.5rem;
151 height: 2.75rem;
152 padding: 0 var(--sl-nav-pad-x);
153 overflow-x: auto;
154 overscroll-behavior-x: contain;
155 scrollbar-width: none;
156 }
157 .g1t-tabs::-webkit-scrollbar {
158 display: none;
159 }
160 .g1t-tabs[data-more-start],
161 .g1t-tabs[data-more-end] {
162 --fade: 2.5rem;
163 mask-image: linear-gradient(
164 to right,
165 transparent,
166 #000 var(--fade-start, 0px),
167 #000 calc(100% - var(--fade-end, 0px)),
168 transparent
169 );
170 }
171 .g1t-tabs[data-more-start] {
172 --fade-start: var(--fade);
173 }
174 .g1t-tabs[data-more-end] {
175 --fade-end: var(--fade);
176 }
177 .g1t-tabs a {
178 display: flex;
179 flex-shrink: 0;
180 align-items: center;
181 border-bottom: 2px solid transparent;
182 font-size: 0.875rem;
183 font-weight: 500;
184 white-space: nowrap;
185 color: var(--g1t-muted);
186 text-decoration: none;
187 transition: color 0.15s;
188 }
189 .g1t-tabs a:hover {
190 color: var(--g1t-fg);
191 }
192 .g1t-tabs a[aria-current='page'] {
193 border-color: var(--g1t-accent);
194 font-weight: 600;
195 color: var(--g1t-fg);
196 }
197</style>