Skip to content

Commit 40f1884

Browse files
committed
feat(docs-site): add custom layout components for editorial theme
Add four new Starlight override components: - Footer.astro: Custom footer with LastUpdated and Pagination - PageTitle.astro: Editorial page header with title and action slots - SiteTitle.astro: Logo and title with light/dark variants - ThemeSelect.astro: Accessible theme toggle with localStorage sync
1 parent c4cbf44 commit 40f1884

4 files changed

Lines changed: 263 additions & 0 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
import LastUpdated from '@astrojs/starlight/components/LastUpdated.astro';
3+
import Pagination from '@astrojs/starlight/components/Pagination.astro';
4+
import config from 'virtual:starlight/user-config';
5+
import { Icon } from '@astrojs/starlight/components';
6+
---
7+
8+
<footer class="recursor-footer sl-flex">
9+
<div class="recursor-footer-meta sl-flex">
10+
<LastUpdated />
11+
</div>
12+
<Pagination />
13+
14+
{
15+
config.credits && (
16+
<a class="recursor-footer-kudos sl-flex" href="https://starlight.astro.build">
17+
<Icon name={'starlight'} /> {Astro.locals.t('builtWithStarlight.label')}
18+
</a>
19+
)
20+
}
21+
</footer>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
import EditLink from './EditLink.astro';
3+
4+
const { entry } = Astro.locals.starlightRoute;
5+
const title = entry.data.title;
6+
const PAGE_TITLE_ID = '_top';
7+
---
8+
9+
<div class="recursor-page-header">
10+
<div class="recursor-page-header__title-group">
11+
<h1 id={PAGE_TITLE_ID}>{title}</h1>
12+
</div>
13+
<div class="recursor-page-header__actions">
14+
<EditLink />
15+
</div>
16+
</div>
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
---
2+
/**
3+
* Custom SiteTitle component for improved logo/title alignment.
4+
*/
5+
6+
import logoLight from '../assets/recursor-logo-light.svg?url';
7+
import logoDark from '../assets/recursor-logo-dark.svg?url';
8+
9+
const { siteTitle, siteTitleHref } = Astro.locals.starlightRoute;
10+
---
11+
12+
<a href={siteTitleHref} class="site-title sl-flex">
13+
<img class="site-title-logo light-logo" alt="ReCursor logo" src={logoLight} />
14+
<img class="site-title-logo dark-logo" alt="ReCursor logo" src={logoDark} />
15+
<span class="site-title-text" translate="no">{siteTitle}</span>
16+
</a>
17+
18+
<style>
19+
.site-title {
20+
align-items: center;
21+
gap: 0.75rem;
22+
padding: 0.25rem 0;
23+
text-decoration: none;
24+
color: var(--sl-color-text);
25+
font-size: 1.0625rem;
26+
font-weight: 600;
27+
letter-spacing: -0.01em;
28+
transition: color 150ms ease;
29+
}
30+
31+
.site-title:hover {
32+
color: var(--sl-color-text-accent);
33+
}
34+
35+
.site-title-logo {
36+
height: 2rem;
37+
width: auto;
38+
flex-shrink: 0;
39+
}
40+
41+
.dark-logo {
42+
display: none;
43+
}
44+
45+
:global(html[data-theme='dark']) .dark-logo {
46+
display: block;
47+
}
48+
49+
:global(html[data-theme='dark']) .light-logo {
50+
display: none;
51+
}
52+
53+
.site-title-text {
54+
display: inline-flex;
55+
align-items: center;
56+
line-height: 1.2;
57+
}
58+
59+
@media (max-width: 640px) {
60+
.site-title {
61+
font-size: 1rem;
62+
gap: 0.5rem;
63+
}
64+
65+
.site-title-logo {
66+
height: 1.75rem;
67+
}
68+
}
69+
</style>
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
---
2+
import { Icon } from '@astrojs/starlight/components';
3+
---
4+
5+
<starlight-theme-toggle>
6+
<button type="button" class="theme-toggle" aria-label="Toggle color theme" title="Toggle color theme">
7+
<span class="sr-only theme-toggle-label">Toggle color theme</span>
8+
<span class="theme-toggle-icon theme-toggle-icon--light" aria-hidden="true">
9+
<Icon name="sun" />
10+
</span>
11+
<span class="theme-toggle-icon theme-toggle-icon--dark" aria-hidden="true">
12+
<Icon name="moon" />
13+
</span>
14+
</button>
15+
</starlight-theme-toggle>
16+
17+
<script>
18+
type Theme = 'light' | 'dark';
19+
20+
const storageKey = 'starlight-theme';
21+
const lightQuery = '(prefers-color-scheme: light)';
22+
23+
function getStoredTheme(): string {
24+
return typeof localStorage !== 'undefined' ? localStorage.getItem(storageKey) ?? '' : '';
25+
}
26+
27+
function getPreferredTheme(): Theme {
28+
return window.matchMedia(lightQuery).matches ? 'light' : 'dark';
29+
}
30+
31+
function getEffectiveTheme(): Theme {
32+
const storedTheme = getStoredTheme();
33+
if (storedTheme === 'light' || storedTheme === 'dark') {
34+
return storedTheme;
35+
}
36+
37+
const datasetTheme = document.documentElement.dataset.theme;
38+
if (datasetTheme === 'light' || datasetTheme === 'dark') {
39+
return datasetTheme;
40+
}
41+
42+
return getPreferredTheme();
43+
}
44+
45+
function applyTheme(theme: Theme): void {
46+
document.documentElement.dataset.theme = theme;
47+
if (typeof localStorage !== 'undefined') {
48+
localStorage.setItem(storageKey, theme);
49+
}
50+
syncThemeToggles(theme);
51+
}
52+
53+
function syncThemeToggles(theme: Theme = getEffectiveTheme()): void {
54+
for (const toggle of document.querySelectorAll('starlight-theme-toggle')) {
55+
const button = toggle.querySelector('button');
56+
const label = toggle.querySelector('.theme-toggle-label');
57+
if (!(button instanceof HTMLButtonElement) || !(label instanceof HTMLElement)) {
58+
continue;
59+
}
60+
61+
const nextTheme = theme === 'light' ? 'dark' : 'light';
62+
const labelText = `Switch to ${nextTheme} mode`;
63+
button.dataset.theme = theme;
64+
button.setAttribute('aria-label', labelText);
65+
button.setAttribute('title', labelText);
66+
label.textContent = labelText;
67+
}
68+
}
69+
70+
class StarlightThemeToggle extends HTMLElement {
71+
private mediaQuery = window.matchMedia(lightQuery);
72+
73+
connectedCallback(): void {
74+
const button = this.querySelector('button');
75+
if (!(button instanceof HTMLButtonElement) || button.dataset.bound === 'true') {
76+
syncThemeToggles();
77+
return;
78+
}
79+
80+
button.addEventListener('click', () => {
81+
const currentTheme = getEffectiveTheme();
82+
applyTheme(currentTheme === 'light' ? 'dark' : 'light');
83+
});
84+
85+
this.mediaQuery.addEventListener('change', () => {
86+
if (!getStoredTheme()) {
87+
const preferredTheme = getPreferredTheme();
88+
document.documentElement.dataset.theme = preferredTheme;
89+
syncThemeToggles(preferredTheme);
90+
}
91+
});
92+
93+
button.dataset.bound = 'true';
94+
syncThemeToggles();
95+
}
96+
}
97+
98+
if (!customElements.get('starlight-theme-toggle')) {
99+
customElements.define('starlight-theme-toggle', StarlightThemeToggle);
100+
}
101+
</script>
102+
103+
<style>
104+
@layer starlight.core {
105+
starlight-theme-toggle {
106+
display: inline-flex;
107+
align-items: center;
108+
}
109+
110+
.theme-toggle {
111+
display: inline-flex;
112+
align-items: center;
113+
justify-content: center;
114+
width: 2.25rem;
115+
height: 2.25rem;
116+
padding: 0;
117+
border: 0;
118+
background: transparent;
119+
color: var(--recursor-ink-subtle, var(--sl-color-gray-3));
120+
cursor: pointer;
121+
border-radius: 999px;
122+
transition:
123+
color 150ms ease,
124+
background-color 150ms ease;
125+
}
126+
127+
.theme-toggle:hover {
128+
color: var(--sl-color-text-accent);
129+
background: color-mix(in srgb, var(--sl-color-accent) 8%, transparent);
130+
}
131+
132+
.theme-toggle:focus-visible {
133+
outline: 2px solid var(--sl-color-accent);
134+
outline-offset: 2px;
135+
}
136+
137+
.theme-toggle-icon {
138+
display: inline-flex;
139+
align-items: center;
140+
justify-content: center;
141+
width: 1rem;
142+
height: 1rem;
143+
}
144+
145+
.theme-toggle-icon--dark {
146+
display: none;
147+
}
148+
149+
:global(html[data-theme='dark']) .theme-toggle-icon--light {
150+
display: none;
151+
}
152+
153+
:global(html[data-theme='dark']) .theme-toggle-icon--dark {
154+
display: inline-flex;
155+
}
156+
}
157+
</style>

0 commit comments

Comments
 (0)