The docs read as sections, each with its own short sidebar. The tabs under the header are Get started, Chat and agents, Code, Projects, Workspaces, Self-hosting and API reference, each holding some of the sidebar's groups (tabs.ts), where Documentation held all of them in one long list; the API's groups are told apart by their pages, since a tag can share a guide group's name. A page's title is larger, with its section above it in lavender; sidebar groups are headed in the page's ink rather than small capitals, and the current page is a lavender pill. Links are the text's own colour on a lavender underline, On this page runs on a rail that marks the heading being read, and link cards turn toward the accent under the pointer. The header's right side is g1t.sh, Source and a Sign up button, then one theme button that moves between Auto, Light and Dark, in place of the full theme select; the phone menu keeps the select, and the two stay in step.
7 files+289−1170/7 viewed
| 7 | 7 | // and when it does not. Here the row is three columns, the outer two always | |
| 8 | 8 | // the same width, so the search sits at the header's centre on every page. | |
| 9 | 9 | import Search from '@astrojs/starlight/components/Search.astro'; | |
| 10 | − | // Auto, Light or Dark, kept in this browser; Auto follows the system. | |
| 11 | − | // On tablets and phones it is at the foot of the menu instead. | |
| 12 | − | import ThemeSelect from '@astrojs/starlight/components/ThemeSelect.astro'; | |
| 13 | 10 | ||
| 14 | − | import { TABS, tabFor } from '../tabs'; | |
| 11 | + | import { NAV } from '../route-data'; | |
| 15 | 12 | import SiteTitle from './SiteTitle.astro'; | |
| 16 | 13 | import 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. | |
| 16 | + | import ThemeToggle from './ThemeToggle.astro'; | |
| 17 | 17 | ||
| 18 | − | const current = tabFor(Astro.locals.starlightRoute.id); | |
| 18 | + | const tabs = NAV.get(Astro.locals.starlightRoute)?.tabs ?? []; | |
| 19 | 19 | --- | |
| 20 | 20 | ||
| 21 | 21 | <div class="g1t-header"> | |
| ⋯ | |||
| 24 | 24 | <div class="g1t-header-search print:hidden"><Search /></div> | |
| 25 | 25 | <div class="g1t-header-links print:hidden"> | |
| 26 | 26 | <SocialIcons /> | |
| 27 | − | <div class="g1t-header-theme"><ThemeSelect /></div> | |
| 27 | + | <div class="g1t-header-theme"><ThemeToggle /></div> | |
| 28 | 28 | </div> | |
| 29 | 29 | </div> | |
| 30 | 30 | <nav class="g1t-tabs" aria-label="Sections"> | |
| 31 | 31 | { | |
| 32 | − | TABS.map((tab) => ( | |
| 33 | − | <a href={tab.href} aria-current={tab === current ? 'page' : undefined}> | |
| 32 | + | tabs.map((tab) => ( | |
| 33 | + | <a href={tab.href} aria-current={tab.current ? 'page' : undefined}> | |
| 34 | 34 | {tab.label} | |
| 35 | 35 | </a> | |
| 36 | 36 | )) | |
| ⋯ | |||
| 38 | 38 | </nav> | |
| 39 | 39 | </div> | |
| 40 | 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 | + | </script> | |
| 49 | + | ||
| 41 | 50 | <style> | |
| 42 | 51 | .g1t-header { | |
| 43 | 52 | display: flex; | |
| ⋯ | |||
| 78 | 87 | display: flex; | |
| 79 | 88 | align-items: center; | |
| 80 | 89 | justify-content: flex-end; | |
| 81 | − | gap: 0.75rem; | |
| 90 | + | gap: 0.5rem; | |
| 82 | 91 | min-width: 0; | |
| 83 | 92 | } | |
| 84 | 93 | .g1t-header-theme { | |
| 85 | 94 | display: flex; | |
| 86 | 95 | align-items: center; | |
| 87 | − | padding-inline-start: 0.75rem; | |
| 88 | − | border-inline-start: 1px solid var(--g1t-line); | |
| 89 | − | height: 1.25rem; | |
| 90 | 96 | } | |
| 91 | − | /* Desktop: the links and the theme select together are wider. On a | |
| 92 | − | narrow desktop the select shows only its icon (styles/g1t.css), so the | |
| 93 | − | search keeps its room. */ | |
| 97 | + | /* Desktop: the links and the theme button together are wider. */ | |
| 94 | 98 | @media (min-width: 50rem) { | |
| 95 | 99 | .g1t-header-top { | |
| 96 | 100 | --g1t-header-side: 17.5rem; | |
| ⋯ | |||
| 127 | 131 | } | |
| 128 | 132 | } | |
| 129 | 133 | .g1t-tabs { | |
| 134 | + | /* The tabs' own offsets are measured from here. */ | |
| 135 | + | position: relative; | |
| 130 | 136 | display: flex; | |
| 131 | 137 | gap: 1.5rem; | |
| 132 | 138 | height: 2.75rem; | |
| ⋯ | |||
| 142 | 148 | display: flex; | |
| 143 | 149 | flex-shrink: 0; | |
| 144 | 150 | align-items: center; | |
| 145 | − | border-bottom: 1.5px solid transparent; | |
| 151 | + | border-bottom: 2px solid transparent; | |
| 146 | 152 | font-size: 0.875rem; | |
| 147 | 153 | font-weight: 500; | |
| 148 | 154 | white-space: nowrap; | |
| ⋯ | |||
| 154 | 160 | color: var(--g1t-fg); | |
| 155 | 161 | } | |
| 156 | 162 | .g1t-tabs a[aria-current='page'] { | |
| 157 | − | border-color: var(--g1t-fg); | |
| 163 | + | border-color: var(--g1t-accent); | |
| 164 | + | font-weight: 600; | |
| 158 | 165 | color: var(--g1t-fg); | |
| 159 | 166 | } | |
| 160 | 167 | </style> | |
| 1 | 1 | --- | |
| 2 | − | // A page's title, what it is about beneath it, and a way to take the page | |
| 2 | + | // A page's title, the section it is in above it, what it is about beneath | |
| 3 | + | // it, and a way to take the page | |
| 3 | 4 | // to an AI assistant: copied as Markdown, read as Markdown, or opened in | |
| 4 | 5 | // Claude or ChatGPT. | |
| 5 | 6 | import { ArrowUpRight, ChevronDown, Copy, FileText, MessageSquare } from '@lucide/astro'; | |
| 6 | 7 | ||
| 8 | + | import { NAV } from '../route-data'; | |
| 9 | + | ||
| 7 | 10 | const { entry, id } = Astro.locals.starlightRoute; | |
| 8 | 11 | const { title, description } = entry.data; | |
| 12 | + | const section = NAV.get(Astro.locals.starlightRoute)?.section; | |
| 9 | 13 | const markdown = `/${id === '' || id === 'index' ? 'index' : id}.md`; | |
| 10 | 14 | const absolute = new URL(markdown, Astro.site).href; | |
| 11 | 15 | const ask = encodeURIComponent(`Read ${absolute} so I can ask questions about it.`); | |
| 12 | 16 | --- | |
| 13 | 17 | ||
| 14 | 18 | <div class="g1t-page-title"> | |
| 19 | + | {section && <p class="g1t-eyebrow">{section}</p>} | |
| 15 | 20 | <div class="g1t-page-title-row"> | |
| 16 | 21 | <h1 id="_top">{title}</h1> | |
| 17 | 22 | <div class="g1t-copy not-content" data-pagefind-ignore> | |
| ⋯ | |||
| 77 | 82 | justify-content: space-between; | |
| 78 | 83 | gap: 1.5rem; | |
| 79 | 84 | } | |
| 85 | + | /* The section, in the accent: where in the docs this page is. In its own | |
| 86 | + | case, never capitals, since the product's name stays lowercase. */ | |
| 87 | + | .g1t-eyebrow { | |
| 88 | + | margin: 0 0 0.6rem; | |
| 89 | + | font-family: var(--g1t-font-mono); | |
| 90 | + | font-size: 0.8125rem; | |
| 91 | + | font-weight: 500; | |
| 92 | + | color: var(--g1t-accent); | |
| 93 | + | } | |
| 80 | 94 | h1 { | |
| 81 | 95 | margin: 0; | |
| 82 | 96 | font-family: var(--g1t-font-display); | |
| 83 | 97 | font-optical-sizing: auto; | |
| 84 | − | font-size: 2rem; | |
| 98 | + | font-size: 2.75rem; | |
| 85 | 99 | font-weight: 600; | |
| 86 | − | letter-spacing: -0.015em; | |
| 87 | − | line-height: 1.2; | |
| 100 | + | letter-spacing: -0.03em; | |
| 101 | + | line-height: 1.08; | |
| 88 | 102 | color: var(--g1t-fg); | |
| 89 | 103 | } | |
| 90 | 104 | .g1t-description { | |
| 91 | − | margin-top: 0.6rem; | |
| 105 | + | margin-top: 0.9rem; | |
| 92 | 106 | max-width: 42rem; | |
| 93 | − | font-size: 1.125rem; | |
| 107 | + | font-size: 1.1875rem; | |
| 94 | 108 | line-height: 1.55; | |
| 95 | 109 | color: var(--g1t-muted); | |
| 96 | 110 | } | |
| ⋯ | |||
| 233 | 247 | } | |
| 234 | 248 | h1 { | |
| 235 | 249 | min-width: 0; | |
| 236 | − | font-size: 1.75rem; | |
| 250 | + | font-size: 2rem; | |
| 237 | 251 | overflow-wrap: anywhere; | |
| 238 | 252 | } | |
| 239 | 253 | .g1t-copy { | |
| 1 | 1 | --- | |
| 2 | − | // The header's right side: where people go from the docs, then the source. | |
| 2 | + | // The header's right side: g1t.sh, its source, and signing up. | |
| 3 | 3 | // g1t's social accounts live in g1t.sh's footer, not here. | |
| 4 | − | import { GitBranch } from '@lucide/astro'; | |
| 4 | + | import { ChevronRight } from '@lucide/astro'; | |
| 5 | 5 | --- | |
| 6 | 6 | ||
| 7 | 7 | <nav class="g1t-nav" aria-label="g1t"> | |
| 8 | 8 | <a href="https://g1t.sh/">g1t.sh</a> | |
| 9 | − | <a href="https://g1t.sh/register" class="g1t-nav-cta">Sign up</a> | |
| 10 | − | <a href="https://g1t.sh/flagon-io/g1t" aria-label="Source on g1t" title="Source on g1t" class="g1t-nav-icon"> | |
| 11 | − | <GitBranch /> | |
| 12 | − | </a> | |
| 9 | + | <a href="https://g1t.sh/flagon-io/g1t">Source</a> | |
| 10 | + | <a href="https://g1t.sh/register" class="g1t-nav-cta">Sign up<ChevronRight /></a> | |
| 13 | 11 | </nav> | |
| 14 | 12 | ||
| 15 | 13 | <style> | |
| ⋯ | |||
| 24 | 22 | display: inline-flex; | |
| 25 | 23 | align-items: center; | |
| 26 | 24 | height: 2rem; | |
| 27 | − | border-radius: 0.4rem; | |
| 25 | + | border-radius: 0.5rem; | |
| 28 | 26 | padding: 0 0.6rem; | |
| 29 | − | font-size: 0.85rem; | |
| 27 | + | font-size: 0.875rem; | |
| 28 | + | font-weight: 500; | |
| 30 | 29 | white-space: nowrap; | |
| 31 | 30 | color: var(--g1t-muted); | |
| 32 | 31 | text-decoration: none; | |
| ⋯ | |||
| 36 | 35 | background: var(--g1t-raised); | |
| 37 | 36 | color: var(--g1t-fg); | |
| 38 | 37 | } | |
| 38 | + | /* The one call to action: a raised button, its chevron nudging on hover. */ | |
| 39 | 39 | nav.g1t-nav .g1t-nav-cta { | |
| 40 | − | margin-left: 0.25rem; | |
| 41 | − | background: var(--g1t-fg); | |
| 42 | − | color: var(--g1t-bg); | |
| 43 | − | font-weight: 500; | |
| 40 | + | gap: 0.15rem; | |
| 41 | + | margin-left: 0.35rem; | |
| 42 | + | padding-inline: 0.75rem 0.5rem; | |
| 43 | + | background: var(--g1t-raised); | |
| 44 | + | box-shadow: inset 0 0 0 1px var(--g1t-line-strong); | |
| 45 | + | color: var(--g1t-fg); | |
| 46 | + | font-weight: 600; | |
| 44 | 47 | } | |
| 45 | 48 | nav.g1t-nav .g1t-nav-cta:hover { | |
| 46 | − | background: var(--g1t-docs-fg-hover); | |
| 47 | − | color: var(--g1t-bg); | |
| 49 | + | background: color-mix(in srgb, var(--g1t-accent) 12%, var(--g1t-raised)); | |
| 50 | + | box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--g1t-accent) 50%, var(--g1t-line-strong)); | |
| 51 | + | color: var(--g1t-fg); | |
| 48 | 52 | } | |
| 49 | − | nav.g1t-nav .g1t-nav-icon { | |
| 50 | − | justify-content: center; | |
| 51 | − | width: 2rem; | |
| 52 | − | padding: 0; | |
| 53 | + | .g1t-nav-cta :global(svg) { | |
| 54 | + | width: 0.875rem; | |
| 55 | + | height: 0.875rem; | |
| 56 | + | color: var(--g1t-muted); | |
| 57 | + | transition: transform 0.15s, color 0.15s; | |
| 53 | 58 | } | |
| 54 | − | .g1t-nav-icon :global(svg) { | |
| 55 | − | width: 1rem; | |
| 56 | − | height: 1rem; | |
| 59 | + | .g1t-nav-cta:hover :global(svg) { | |
| 60 | + | transform: translateX(2px); | |
| 61 | + | color: var(--g1t-accent); | |
| 57 | 62 | } | |
| 58 | − | /* Tablets and phones: in the header g1t.sh's link gives way (the menu | |
| 59 | − | keeps it), and the icons grow to a size a finger can hit. */ | |
| 63 | + | /* Tablets and phones: in the header only Sign up stays (the menu keeps | |
| 64 | + | the rest). */ | |
| 60 | 65 | @media (max-width: 49.99rem) { | |
| 61 | − | :global(.g1t-header) nav.g1t-nav a:not(.g1t-nav-icon):not(.g1t-nav-cta) { | |
| 66 | + | :global(.g1t-header) nav.g1t-nav a:not(.g1t-nav-cta) { | |
| 62 | 67 | display: none; | |
| 63 | − | } | |
| 64 | − | nav.g1t-nav .g1t-nav-icon { | |
| 65 | − | width: 2.5rem; | |
| 66 | − | height: 2.5rem; | |
| 67 | 68 | } | |
| 68 | 69 | :global(.sidebar-content) nav.g1t-nav a { | |
| 69 | 70 | height: 2.5rem; | |
| 1 | + | --- | |
| 2 | + | // The header's theme button: one icon, for Auto (the system's), Light or | |
| 3 | + | // Dark, and a click moves to the next. The same choice, kept in the same | |
| 4 | + | // place, as Starlight's theme select at the foot of the phone menu, so | |
| 5 | + | // the two stay in step. | |
| 6 | + | import { Monitor, Moon, Sun } from '@lucide/astro'; | |
| 7 | + | --- | |
| 8 | + | ||
| 9 | + | <button type="button" class="g1t-theme" data-g1t-theme aria-label="Theme: Auto"> | |
| 10 | + | <Monitor class="g1t-theme-auto" /> | |
| 11 | + | <Sun class="g1t-theme-light" /> | |
| 12 | + | <Moon class="g1t-theme-dark" /> | |
| 13 | + | </button> | |
| 14 | + | ||
| 15 | + | <script is:inline> | |
| 16 | + | // Before the first paint, so the button never shows the wrong icon. | |
| 17 | + | (() => { | |
| 18 | + | let stored = null; | |
| 19 | + | try { | |
| 20 | + | stored = localStorage.getItem('starlight-theme'); | |
| 21 | + | } catch {} | |
| 22 | + | const choice = stored === 'light' || stored === 'dark' ? stored : 'auto'; | |
| 23 | + | for (const button of document.querySelectorAll('[data-g1t-theme]')) button.dataset.choice = choice; | |
| 24 | + | })(); | |
| 25 | + | </script> | |
| 26 | + | ||
| 27 | + | <script> | |
| 28 | + | type Theme = 'auto' | 'light' | 'dark'; | |
| 29 | + | const NEXT: Record<Theme, Theme> = { auto: 'light', light: 'dark', dark: 'auto' }; | |
| 30 | + | const NAMES: Record<Theme, string> = { auto: 'Auto', light: 'Light', dark: 'Dark' }; | |
| 31 | + | const KEY = 'starlight-theme'; | |
| 32 | + | ||
| 33 | + | const load = (): Theme => { | |
| 34 | + | try { | |
| 35 | + | const stored = localStorage.getItem(KEY); | |
| 36 | + | return stored === 'light' || stored === 'dark' ? stored : 'auto'; | |
| 37 | + | } catch { | |
| 38 | + | return 'auto'; | |
| 39 | + | } | |
| 40 | + | }; | |
| 41 | + | ||
| 42 | + | function show(theme: Theme) { | |
| 43 | + | document.querySelectorAll<HTMLButtonElement>('[data-g1t-theme]').forEach((button) => { | |
| 44 | + | button.dataset.choice = theme; | |
| 45 | + | button.setAttribute('aria-label', `Theme: ${NAMES[theme]}`); | |
| 46 | + | }); | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | function choose(theme: Theme) { | |
| 50 | + | const system = matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'; | |
| 51 | + | document.documentElement.dataset.theme = theme === 'auto' ? system : theme; | |
| 52 | + | try { | |
| 53 | + | localStorage.setItem(KEY, theme === 'auto' ? '' : theme); | |
| 54 | + | } catch {} | |
| 55 | + | // Starlight's own select, in the phone menu, shows the same choice. | |
| 56 | + | (window as { StarlightThemeProvider?: { updatePickers(theme: Theme): void } }).StarlightThemeProvider?.updatePickers( | |
| 57 | + | theme, | |
| 58 | + | ); | |
| 59 | + | show(theme); | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | show(load()); | |
| 63 | + | document.querySelectorAll<HTMLButtonElement>('[data-g1t-theme]').forEach((button) => { | |
| 64 | + | button.addEventListener('click', () => choose(NEXT[load()])); | |
| 65 | + | }); | |
| 66 | + | // A choice made in the phone menu's select. | |
| 67 | + | document.addEventListener('change', (event) => { | |
| 68 | + | if ((event.target as Element).closest('starlight-theme-select')) show(load()); | |
| 69 | + | }); | |
| 70 | + | </script> | |
| 71 | + | ||
| 72 | + | <style> | |
| 73 | + | .g1t-theme { | |
| 74 | + | display: inline-flex; | |
| 75 | + | align-items: center; | |
| 76 | + | justify-content: center; | |
| 77 | + | width: 2rem; | |
| 78 | + | height: 2rem; | |
| 79 | + | border: 0; | |
| 80 | + | border-radius: 0.5rem; | |
| 81 | + | padding: 0; | |
| 82 | + | background: transparent; | |
| 83 | + | color: var(--g1t-muted); | |
| 84 | + | cursor: pointer; | |
| 85 | + | transition: color 0.15s, background 0.15s; | |
| 86 | + | } | |
| 87 | + | .g1t-theme:hover { | |
| 88 | + | background: var(--g1t-raised); | |
| 89 | + | color: var(--g1t-fg); | |
| 90 | + | } | |
| 91 | + | .g1t-theme:focus-visible { | |
| 92 | + | outline: 2px solid var(--g1t-accent); | |
| 93 | + | outline-offset: 1px; | |
| 94 | + | } | |
| 95 | + | .g1t-theme :global(svg) { | |
| 96 | + | display: none; | |
| 97 | + | width: 1rem; | |
| 98 | + | height: 1rem; | |
| 99 | + | } | |
| 100 | + | .g1t-theme[data-choice='auto'] :global(.g1t-theme-auto), | |
| 101 | + | .g1t-theme[data-choice='light'] :global(.g1t-theme-light), | |
| 102 | + | .g1t-theme[data-choice='dark'] :global(.g1t-theme-dark) { | |
| 103 | + | display: block; | |
| 104 | + | } | |
| 105 | + | </style> |
| 1 | 1 | import { defineRouteMiddleware } from '@astrojs/starlight/route-data'; | |
| 2 | 2 | import { ogVersion } from '@g1t/contracts/og'; | |
| 3 | 3 | ||
| 4 | − | import { tabFor } from './tabs'; | |
| 4 | + | import { TABS, type Tab, tabForGroup } from './tabs'; | |
| 5 | 5 | ||
| 6 | 6 | type Entry = { type: string; label?: string; href?: string; isCurrent?: boolean; entries?: Entry[] }; | |
| 7 | 7 | ||
| ⋯ | |||
| 34 | 34 | return undefined; | |
| 35 | 35 | } | |
| 36 | 36 | ||
| 37 | + | /** What the header and the page's title show: the tabs, and where the page sits. */ | |
| 38 | + | export type Nav = { | |
| 39 | + | tabs: { label: string; href: string; current: boolean }[]; | |
| 40 | + | /** The sidebar group holding the page, or its tab's name, shown above its title. */ | |
| 41 | + | section?: string; | |
| 42 | + | }; | |
| 43 | + | ||
| 44 | + | /** Each route's Nav, set here for Header.astro and PageTitle.astro. */ | |
| 45 | + | export const NAV = new WeakMap<object, Nav>(); | |
| 46 | + | ||
| 37 | 47 | /** | |
| 38 | − | * Each page's sidebar holds only its tab's groups: those whose pages | |
| 39 | − | * belong to the same tab. Groups are told apart by their pages, not their | |
| 40 | − | * labels, since a guide group and an API group can share a name. | |
| 48 | + | * The tab a top-level sidebar group belongs to. The API's groups are told | |
| 49 | + | * apart by their pages, not their labels, since an API tag can share a | |
| 50 | + | * guide group's name. | |
| 51 | + | */ | |
| 52 | + | function tabOf(group: Entry): Tab { | |
| 53 | + | const slug = firstSlug(group.entries ?? []) ?? ''; | |
| 54 | + | return slug.startsWith('reference/') ? TABS[TABS.length - 1] : tabForGroup(group.label ?? ''); | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | /** Whether the current page is in these entries, at any depth. */ | |
| 58 | + | function holdsCurrent(entries: Entry[]): boolean { | |
| 59 | + | return entries.some((entry) => entry.isCurrent || holdsCurrent(entry.entries ?? [])); | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | /** | |
| 63 | + | * Each page's sidebar holds only its tab's groups. | |
| 41 | 64 | * | |
| 42 | 65 | * Each page also gets its own social card: its title, its section and its | |
| 43 | 66 | * description, drawn by the og service. | |
| 44 | 67 | */ | |
| 45 | 68 | export const onRequest = defineRouteMiddleware((context) => { | |
| 46 | 69 | const route = context.locals.starlightRoute; | |
| 47 | − | const section = sectionOf(route.sidebar as Entry[]); | |
| 48 | − | const tab = tabFor(route.id); | |
| 49 | − | route.sidebar = route.sidebar.filter((entry) => { | |
| 50 | − | if (entry.type !== 'group') return false; | |
| 51 | − | const slug = firstSlug(entry.entries as Entry[]); | |
| 52 | − | return slug !== undefined && tabFor(slug) === tab; | |
| 70 | + | const groups = (route.sidebar as Entry[]).filter((entry) => entry.type === 'group'); | |
| 71 | + | const section = sectionOf(groups); | |
| 72 | + | // A page in no group, such as the home page, is in the first tab. | |
| 73 | + | const holder = groups.find((group) => holdsCurrent(group.entries ?? [])); | |
| 74 | + | const tab = holder ? tabOf(holder) : TABS[0]; | |
| 75 | + | // A tab opens on the first page of its first group; the first tab, on the home page. | |
| 76 | + | const tabs = TABS.map((each) => { | |
| 77 | + | const group = each === TABS[0] ? undefined : groups.find((entry) => tabOf(entry) === each); | |
| 78 | + | const slug = group ? firstSlug(group.entries ?? []) : undefined; | |
| 79 | + | return { label: each.label, href: slug === undefined ? '/' : `/${slug}/`, current: each === tab }; | |
| 53 | 80 | }); | |
| 81 | + | NAV.set(route, { tabs, section: section ?? (route.id === '' ? undefined : tab.label) }); | |
| 82 | + | route.sidebar = route.sidebar.filter((entry) => entry.type === 'group' && tabOf(entry as Entry) === tab); | |
| 54 | 83 | ||
| 55 | 84 | const { title, description } = route.entry.data; | |
| 56 | 85 | const card = new URL('/docs', OG); | |
| 45 | 45 | --sl-menu-button-size: 2.5rem; | |
| 46 | 46 | } | |
| 47 | 47 | ||
| 48 | − | /* Starlight's theme select (Auto, Light, Dark): in the header's links, and | |
| 49 | − | at the foot of the phone menu. Drawn like the links beside it. */ | |
| 48 | + | /* Starlight's theme select (Auto, Light, Dark), at the foot of the phone | |
| 49 | + | menu; the header has its own button (components/ThemeToggle.astro). */ | |
| 50 | 50 | starlight-theme-select label { | |
| 51 | 51 | color: var(--g1t-muted); | |
| 52 | 52 | font-size: 0.85rem; | |
| ⋯ | |||
| 60 | 60 | starlight-theme-select option { | |
| 61 | 61 | background: var(--g1t-surface); | |
| 62 | 62 | color: var(--g1t-fg); | |
| 63 | − | } | |
| 64 | − | /* A narrow desktop: in the header, only the select's icon and caret; the | |
| 65 | − | choice is still its accessible name and in the list it opens. */ | |
| 66 | − | @media (min-width: 50rem) and (max-width: 63.99rem) { | |
| 67 | − | .g1t-header-theme starlight-theme-select select { | |
| 68 | − | width: 3.625rem; | |
| 69 | − | color: transparent; | |
| 70 | − | } | |
| 71 | 63 | } | |
| 72 | 64 | ||
| 73 | 65 | /* --- Header --------------------------------------------------------------- */ | |
| ⋯ | |||
| 161 | 153 | /* --- Sidebar -------------------------------------------------------------- */ | |
| 162 | 154 | ||
| 163 | 155 | .sidebar-content { | |
| 164 | − | padding-top: 1.25rem; | |
| 156 | + | padding-top: 1.5rem; | |
| 165 | 157 | } | |
| 166 | 158 | /* Groups always show their pages: no caret, no folding. */ | |
| 167 | 159 | .sidebar-content details > summary { | |
| ⋯ | |||
| 172 | 164 | display: none; | |
| 173 | 165 | } | |
| 174 | 166 | .sidebar-content .top-level > li + li { | |
| 175 | − | margin-top: 1rem; | |
| 167 | + | margin-top: 1.75rem; | |
| 176 | 168 | } | |
| 177 | − | /* Group labels: small and quiet, like section names, not links. */ | |
| 169 | + | /* Group labels: headings over their pages, in the page's own ink. */ | |
| 178 | 170 | .sidebar-content details > summary .group-label .large, | |
| 179 | 171 | .sidebar-content .top-level > li > details > summary { | |
| 180 | − | font-size: 0.75rem; | |
| 172 | + | font-size: 0.875rem; | |
| 181 | 173 | font-weight: 600; | |
| 182 | − | letter-spacing: 0.06em; | |
| 183 | − | text-transform: uppercase; | |
| 184 | − | color: var(--g1t-faint); | |
| 174 | + | color: var(--g1t-fg); | |
| 185 | 175 | } | |
| 176 | + | .sidebar-content .top-level > li > details > summary { | |
| 177 | + | padding-inline: 0.75rem; | |
| 178 | + | margin-bottom: 0.35rem; | |
| 179 | + | } | |
| 186 | 180 | .sidebar-content ul ul, | |
| 187 | 181 | .sidebar-content ul ul li { | |
| 188 | 182 | border-inline-start: 0; | |
| ⋯ | |||
| 190 | 184 | padding-inline-start: 0; | |
| 191 | 185 | } | |
| 192 | 186 | .sidebar-content a { | |
| 193 | − | border-radius: 0.375rem; | |
| 194 | − | padding: 0.32rem 0.6rem; | |
| 187 | + | border-radius: 0.5rem; | |
| 188 | + | padding: 0.4rem 0.75rem; | |
| 195 | 189 | font-size: 0.875rem; | |
| 196 | 190 | color: var(--g1t-muted); | |
| 191 | + | transition: background 0.15s, color 0.15s; | |
| 197 | 192 | } | |
| 198 | 193 | .sidebar-content a:hover { | |
| 199 | 194 | background: var(--g1t-surface); | |
| 200 | 195 | color: var(--g1t-fg); | |
| 201 | 196 | } | |
| 202 | − | /* The current page: brighter text and a lavender mark, not a filled bar. | |
| 203 | − | Square on the mark's side, so the mark is a straight line. */ | |
| 197 | + | /* The current page: a lavender pill. */ | |
| 204 | 198 | .sidebar-content a[aria-current='page'], | |
| 205 | 199 | .sidebar-content a[aria-current='page']:hover { | |
| 206 | − | border-radius: 0 0.375rem 0.375rem 0; | |
| 207 | − | background: var(--g1t-surface); | |
| 208 | − | color: var(--g1t-fg); | |
| 209 | − | font-weight: 500; | |
| 210 | − | box-shadow: inset 2px 0 0 var(--g1t-accent); | |
| 200 | + | background: var(--g1t-accent-low); | |
| 201 | + | color: var(--g1t-docs-accent-high); | |
| 202 | + | font-weight: 600; | |
| 211 | 203 | } | |
| 212 | 204 | ||
| 213 | 205 | /* --- Content -------------------------------------------------------------- */ | |
| 214 | 206 | ||
| 215 | 207 | .content-panel { | |
| 216 | − | padding-top: 2rem; | |
| 208 | + | padding-top: 2.75rem; | |
| 217 | 209 | } | |
| 218 | 210 | /* The title and the page read as one, without a rule between them. */ | |
| 219 | 211 | .content-panel + .content-panel { | |
| ⋯ | |||
| 234 | 226 | } | |
| 235 | 227 | /* Sections open with space, not rules. */ | |
| 236 | 228 | .sl-markdown-content .sl-heading-wrapper.level-h2 { | |
| 237 | − | margin-top: 3rem; | |
| 229 | + | margin-top: 3.5rem; | |
| 238 | 230 | } | |
| 239 | 231 | .sl-markdown-content h2 { | |
| 240 | − | font-size: 1.5rem; | |
| 232 | + | font-size: 1.75rem; | |
| 241 | 233 | font-weight: 600; | |
| 242 | − | letter-spacing: -0.01em; | |
| 234 | + | letter-spacing: -0.02em; | |
| 235 | + | line-height: 1.2; | |
| 243 | 236 | color: var(--g1t-fg); | |
| 244 | 237 | } | |
| 245 | 238 | .sl-markdown-content h3 { | |
| ⋯ | |||
| 253 | 246 | .sl-markdown-content li { | |
| 254 | 247 | color: var(--g1t-fg-soft); | |
| 255 | 248 | } | |
| 249 | + | /* Links: the text's own ink, a little heavier, on a lavender underline. */ | |
| 256 | 250 | .sl-markdown-content a:not(.card, .sl-link-button, .sl-link-card) { | |
| 257 | − | color: var(--g1t-docs-link); | |
| 258 | − | text-decoration-color: color-mix(in srgb, var(--g1t-accent) 45%, transparent); | |
| 259 | − | text-underline-offset: 0.2em; | |
| 251 | + | color: var(--g1t-fg); | |
| 252 | + | font-weight: 500; | |
| 253 | + | text-decoration-color: color-mix(in srgb, var(--g1t-accent) 55%, transparent); | |
| 254 | + | text-decoration-thickness: 1.5px; | |
| 255 | + | text-underline-offset: 0.25em; | |
| 256 | + | transition: text-decoration-color 0.15s, color 0.15s; | |
| 260 | 257 | } | |
| 261 | 258 | .sl-markdown-content a:not(.card, .sl-link-button, .sl-link-card):hover { | |
| 259 | + | color: var(--g1t-docs-link); | |
| 262 | 260 | text-decoration-color: var(--g1t-accent); | |
| 263 | 261 | } | |
| 264 | 262 | .sl-markdown-content :not(pre) > code { | |
| ⋯ | |||
| 347 | 345 | ||
| 348 | 346 | /* --- On this page --------------------------------------------------------- */ | |
| 349 | 347 | ||
| 348 | + | .right-sidebar-panel { | |
| 349 | + | padding-top: 2.75rem; | |
| 350 | + | } | |
| 350 | 351 | .right-sidebar-panel h2 { | |
| 351 | − | font-size: 0.75rem; | |
| 352 | + | font-size: 0.875rem; | |
| 352 | 353 | font-weight: 600; | |
| 353 | − | letter-spacing: 0.06em; | |
| 354 | − | text-transform: uppercase; | |
| 355 | − | color: var(--g1t-faint); | |
| 354 | + | color: var(--g1t-fg); | |
| 355 | + | } | |
| 356 | + | /* The headings on a rail; the one being read is marked on it. */ | |
| 357 | + | .right-sidebar-panel starlight-toc > nav > ul { | |
| 358 | + | border-inline-start: 1px solid var(--g1t-line); | |
| 356 | 359 | } | |
| 357 | 360 | .right-sidebar-panel a { | |
| 361 | + | margin-inline-start: -1px; | |
| 362 | + | border-inline-start: 1px solid transparent; | |
| 363 | + | border-radius: 0; | |
| 364 | + | padding-inline-start: calc(0.75rem + var(--depth) * 0.75rem); | |
| 358 | 365 | font-size: 0.8125rem; | |
| 359 | 366 | color: var(--g1t-muted); | |
| 367 | + | transition: color 0.15s, border-color 0.15s; | |
| 360 | 368 | } | |
| 361 | 369 | .right-sidebar-panel a:hover { | |
| 362 | 370 | color: var(--g1t-fg); | |
| 363 | 371 | } | |
| 364 | 372 | .right-sidebar-panel a[aria-current='true'] { | |
| 373 | + | border-color: var(--g1t-accent); | |
| 365 | 374 | color: var(--g1t-docs-link); | |
| 375 | + | font-weight: 500; | |
| 366 | 376 | } | |
| 367 | 377 | ||
| 368 | 378 | /* --- Cards and buttons ---------------------------------------------------- */ | |
| ⋯ | |||
| 372 | 382 | border-radius: 1rem; | |
| 373 | 383 | background: var(--g1t-surface); | |
| 374 | 384 | border: 1px solid var(--g1t-line); | |
| 375 | − | transition: border-color 0.15s; | |
| 385 | + | transition: border-color 0.15s, background 0.15s, transform 0.15s; | |
| 376 | 386 | } | |
| 387 | + | /* A link card lifts toward the accent under the pointer. */ | |
| 377 | 388 | .sl-link-card:hover { | |
| 378 | − | border-color: var(--g1t-line-strong); | |
| 379 | − | background: var(--g1t-surface); | |
| 389 | + | border-color: color-mix(in srgb, var(--g1t-accent) 45%, var(--g1t-line)); | |
| 390 | + | background: color-mix(in srgb, var(--g1t-accent) 4%, var(--g1t-surface)); | |
| 391 | + | } | |
| 392 | + | .sl-link-card:hover .icon { | |
| 393 | + | color: var(--g1t-accent); | |
| 380 | 394 | } | |
| 381 | 395 | .sl-link-button { | |
| 382 | 396 | border-radius: 0.5rem; | |
| 1 | 1 | /** | |
| 2 | 2 | * The tabs under the header. Each shows its own part of the sidebar: the | |
| 3 | − | * groups whose pages fit its `match`. A page belongs to the tab whose | |
| 4 | − | * `match` it fits. | |
| 3 | + | * groups named in its `groups`, in the sidebar's order. A page belongs to | |
| 4 | + | * the tab holding its group; the home page, in no group, to the first. | |
| 5 | + | * The API reference tab takes every group not named here, since the | |
| 6 | + | * operations' groups are named after the API's tags. | |
| 5 | 7 | */ | |
| 6 | − | export type Tab = { label: string; href: string; match: (slug: string) => boolean }; | |
| 8 | + | export type Tab = { label: string; groups: string[] }; | |
| 7 | 9 | ||
| 8 | 10 | export const TABS: Tab[] = [ | |
| 9 | − | { | |
| 10 | − | label: 'Documentation', | |
| 11 | − | href: '/', | |
| 12 | − | match: (slug) => !slug.startsWith('reference/'), | |
| 13 | − | }, | |
| 14 | − | { | |
| 15 | − | label: 'API reference', | |
| 16 | − | href: '/reference/api/', | |
| 17 | − | match: (slug) => slug.startsWith('reference/'), | |
| 18 | − | }, | |
| 11 | + | { label: 'Get started', groups: ['Get started', 'About'] }, | |
| 12 | + | { label: 'Chat and agents', groups: ['Chat and agents', 'Agents on code'] }, | |
| 13 | + | { label: 'Code', groups: ['Landing changes', 'Security'] }, | |
| 14 | + | { label: 'Projects', groups: ['Projects'] }, | |
| 15 | + | { label: 'Workspaces', groups: ['Workspaces', 'Connect your tools'] }, | |
| 16 | + | { label: 'Self-hosting', groups: ['Run g1t yourself'] }, | |
| 17 | + | { label: 'API reference', groups: [] }, | |
| 19 | 18 | ]; | |
| 20 | 19 | ||
| 21 | − | export function tabFor(slug: string): Tab { | |
| 22 | − | return TABS.find((tab) => tab.match(slug)) ?? TABS[0]; | |
| 20 | + | const API = TABS[TABS.length - 1]; | |
| 21 | + | ||
| 22 | + | /** The tab that holds the sidebar group with this label. */ | |
| 23 | + | export function tabForGroup(label: string): Tab { | |
| 24 | + | return TABS.find((tab) => tab.groups.includes(label)) ?? API; | |
| 23 | 25 | } |