@rxova/brand 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/astro.css +4 -0
- package/src/components/SiteFooter.astro +99 -61
- package/src/components/SiteShell.astro +321 -0
- package/src/components/ThemeToggle.astro +63 -13
- package/src/footer.css +138 -0
- package/src/sites.ts +1 -1
- package/src/starlight.css +4 -50
package/package.json
CHANGED
package/src/astro.css
CHANGED
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
@import './tokens.css';
|
|
16
|
+
/* The shared site footer, which /blog, /updates and the landing all render. It
|
|
17
|
+
is the one piece of chrome common to these surfaces and the Starlight ones,
|
|
18
|
+
so its rules sit in their own file and both entry points pull them in. */
|
|
19
|
+
@import './footer.css';
|
|
16
20
|
|
|
17
21
|
:root {
|
|
18
22
|
--bg: var(--rx-bg);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
/**
|
|
3
|
-
* The
|
|
3
|
+
* The rxova footer, shared by every surface.
|
|
4
4
|
*
|
|
5
5
|
* Before this, the landing had a single dot-separated link row and the three
|
|
6
6
|
* docs sites had a bare copyright line (journey went further and hid its footer
|
|
@@ -8,6 +8,14 @@
|
|
|
8
8
|
* make one origin feel like one site — so it carries the full project list on
|
|
9
9
|
* every page.
|
|
10
10
|
*
|
|
11
|
+
* Shape: a brand block that says what rxova is, the link columns, then a bottom
|
|
12
|
+
* bar with the copyright and the legal links. The legal links are down there
|
|
13
|
+
* rather than in a column because that is where a reader looks for them, and it
|
|
14
|
+
* keeps the columns to things someone might actually want to click.
|
|
15
|
+
*
|
|
16
|
+
* Styles are in `../footer.css`, imported by both `astro.css` and
|
|
17
|
+
* `starlight.css` — see that file for why they are not scoped here.
|
|
18
|
+
*
|
|
11
19
|
* All cross-project hrefs are absolute; see sites.ts for why.
|
|
12
20
|
*/
|
|
13
21
|
import { PROJECTS, RXOVA_ORIGIN, getProject, siteUrl, type ProjectId } from '../sites.ts'
|
|
@@ -25,72 +33,102 @@ const year = new Date().getFullYear()
|
|
|
25
33
|
---
|
|
26
34
|
|
|
27
35
|
<footer class="rx-footer">
|
|
28
|
-
<div class="rx-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
36
|
+
<div class="rx-footer__top">
|
|
37
|
+
<div class="rx-footer__brand">
|
|
38
|
+
<a class="rx-footer__mark" href={RXOVA_ORIGIN}>
|
|
39
|
+
<img src={siteUrl('/rxova-logo-256.png')} alt="" width="28" height="28" loading="lazy" />
|
|
40
|
+
<span>rxova</span>
|
|
41
|
+
</a>
|
|
42
|
+
<p class="rx-footer__blurb">
|
|
43
|
+
Open-source React libraries, documented in one place. MIT licensed, built in the open.
|
|
44
|
+
</p>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div class="rx-footer__columns">
|
|
48
|
+
{
|
|
49
|
+
docs && docs.length > 0 && (
|
|
50
|
+
<div>
|
|
51
|
+
<h2 class="rx-footer__title">Docs</h2>
|
|
52
|
+
<ul class="rx-footer__list">
|
|
53
|
+
{docs.map((link) => (
|
|
54
|
+
<li>
|
|
55
|
+
<a href={link.href}>{link.label}</a>
|
|
56
|
+
</li>
|
|
57
|
+
))}
|
|
58
|
+
</ul>
|
|
59
|
+
</div>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
<div>
|
|
64
|
+
<h2 class="rx-footer__title">Projects</h2>
|
|
65
|
+
<ul class="rx-footer__list">
|
|
66
|
+
{
|
|
67
|
+
PROJECTS.map((p) => (
|
|
35
68
|
<li>
|
|
36
|
-
<a href={
|
|
69
|
+
<a href={`${RXOVA_ORIGIN}${p.mount}`}>{p.label}</a>
|
|
37
70
|
</li>
|
|
38
|
-
))
|
|
39
|
-
|
|
40
|
-
</
|
|
41
|
-
|
|
42
|
-
}
|
|
71
|
+
))
|
|
72
|
+
}
|
|
73
|
+
</ul>
|
|
74
|
+
</div>
|
|
43
75
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
76
|
+
{
|
|
77
|
+
self && (
|
|
78
|
+
<div>
|
|
79
|
+
<h2 class="rx-footer__title">Community</h2>
|
|
80
|
+
<ul class="rx-footer__list">
|
|
81
|
+
<li>
|
|
82
|
+
<a href={self.repo}>GitHub</a>
|
|
83
|
+
</li>
|
|
84
|
+
<li>
|
|
85
|
+
<a href={`${self.repo}/issues`}>Issues</a>
|
|
86
|
+
</li>
|
|
87
|
+
<li>
|
|
88
|
+
<a href={`${self.repo}/discussions`}>Discussions</a>
|
|
89
|
+
</li>
|
|
90
|
+
<li>
|
|
91
|
+
<a href={self.npm}>npm</a>
|
|
92
|
+
</li>
|
|
93
|
+
<li>
|
|
94
|
+
<a href={`${self.repo}/blob/main/CONTRIBUTING.md`}>Contributing</a>
|
|
95
|
+
</li>
|
|
96
|
+
</ul>
|
|
97
|
+
</div>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
57
100
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
<li>
|
|
73
|
-
<a href={self.npm}>npm</a>
|
|
74
|
-
</li>
|
|
75
|
-
<li>
|
|
76
|
-
<a href={`${self.repo}/blob/main/CONTRIBUTING.md`}>Contributing</a>
|
|
77
|
-
</li>
|
|
78
|
-
</ul>
|
|
79
|
-
</div>
|
|
80
|
-
)
|
|
81
|
-
}
|
|
101
|
+
{
|
|
102
|
+
/* Blog and Updates are here on every surface, docs sites included: they
|
|
103
|
+
were reachable only from the umbrella landing, which meant a reader
|
|
104
|
+
who arrived on a docs page from a search result never learned the
|
|
105
|
+
rest of the site existed. */
|
|
106
|
+
}
|
|
107
|
+
<div>
|
|
108
|
+
<h2 class="rx-footer__title">Site</h2>
|
|
109
|
+
<ul class="rx-footer__list">
|
|
110
|
+
<li><a href={RXOVA_ORIGIN}>Home</a></li>
|
|
111
|
+
<li><a href={siteUrl('/blog')}>Blog</a></li>
|
|
112
|
+
<li><a href={siteUrl('/updates')}>Updates</a></li>
|
|
113
|
+
</ul>
|
|
114
|
+
</div>
|
|
82
115
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
</ul>
|
|
116
|
+
<div>
|
|
117
|
+
<h2 class="rx-footer__title">Connect</h2>
|
|
118
|
+
<ul class="rx-footer__list">
|
|
119
|
+
<li><a href="https://github.com/rxova">GitHub</a></li>
|
|
120
|
+
<li><a href="https://www.npmjs.com/org/rxova">npm</a></li>
|
|
121
|
+
<li><a href="mailto:rxova@proton.me">rxova@proton.me</a></li>
|
|
122
|
+
</ul>
|
|
123
|
+
</div>
|
|
92
124
|
</div>
|
|
93
125
|
</div>
|
|
94
126
|
|
|
95
|
-
<
|
|
127
|
+
<div class="rx-footer__legal">
|
|
128
|
+
<p>© {year} rxova · MIT licensed</p>
|
|
129
|
+
<div class="rx-footer__legal-links">
|
|
130
|
+
<a href={siteUrl('/privacy')}>Privacy</a>
|
|
131
|
+
<a href={siteUrl('/terms')}>Terms</a>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
96
134
|
</footer>
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* The shell for rxova.org's non-Starlight surfaces — /blog and /updates.
|
|
4
|
+
*
|
|
5
|
+
* It lived in both packages as a 244-line file that differed on four lines, which
|
|
6
|
+
* is exactly how the menu came to render in a different order on each: a fix has to
|
|
7
|
+
* be made twice, and once was enough to forget.
|
|
8
|
+
*
|
|
9
|
+
* Not for the docs sites — those are Starlight and take their chrome from
|
|
10
|
+
* `sharedStarlightConfig`. This is for the surfaces that are plain Astro.
|
|
11
|
+
*/
|
|
12
|
+
import '../fonts.css'
|
|
13
|
+
import '../astro.css'
|
|
14
|
+
|
|
15
|
+
import { RXOVA_ORIGIN, siteUrl } from '../sites.ts'
|
|
16
|
+
|
|
17
|
+
import SiteFooter from './SiteFooter.astro'
|
|
18
|
+
import ThemeScript from './ThemeScript.astro'
|
|
19
|
+
import ThemeToggle from './ThemeToggle.astro'
|
|
20
|
+
|
|
21
|
+
interface Props {
|
|
22
|
+
title: string
|
|
23
|
+
description: string
|
|
24
|
+
/** Canonical path, e.g. `/blog/why-rxova-has-a-blog`. */
|
|
25
|
+
path: string
|
|
26
|
+
ogType?: 'website' | 'article'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const { title, description, path, ogType = 'website' } = Astro.props
|
|
30
|
+
const canonical = `${RXOVA_ORIGIN}${path}`
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The menu, in one fixed order for every surface.
|
|
34
|
+
*
|
|
35
|
+
* Which item is current is *derived* from the base the surface was built for, not
|
|
36
|
+
* declared per package — declaring it is what let the two drift into different
|
|
37
|
+
* orders. `BASE_URL` is substituted by the consuming app's build, so this is
|
|
38
|
+
* `/blog` inside @rxova/blog and `/updates` inside @rxova/updates.
|
|
39
|
+
*/
|
|
40
|
+
const base = import.meta.env.BASE_URL.replace(/\/$/, '')
|
|
41
|
+
|
|
42
|
+
const SECTIONS = [
|
|
43
|
+
{ label: 'Blog', path: '/blog' },
|
|
44
|
+
{ label: 'Updates', path: '/updates' },
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A URL inside this surface.
|
|
49
|
+
*
|
|
50
|
+
* Anything *leaving* it goes through `siteUrl`: a bare `/updates` would resolve
|
|
51
|
+
* against the mount and point at `/blog/updates`.
|
|
52
|
+
*/
|
|
53
|
+
const inside = (p = '') => `${base}/${p}`.replace(/\/{2,}/g, '/')
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
<!doctype html>
|
|
57
|
+
<html lang="en">
|
|
58
|
+
<head>
|
|
59
|
+
<meta charset="utf-8" />
|
|
60
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
61
|
+
<link rel="icon" type="image/png" href={siteUrl('/rxova-logo-256.png')} />
|
|
62
|
+
<link rel="apple-touch-icon" href={siteUrl('/rxova-logo-256.png')} />
|
|
63
|
+
<link rel="canonical" href={canonical} />
|
|
64
|
+
<title>{title}</title>
|
|
65
|
+
<meta name="description" content={description} />
|
|
66
|
+
<meta property="og:title" content={title} />
|
|
67
|
+
<meta property="og:description" content={description} />
|
|
68
|
+
<meta property="og:type" content={ogType} />
|
|
69
|
+
<meta property="og:url" content={canonical} />
|
|
70
|
+
<meta property="og:image" content={siteUrl('/og/rxova.png')} />
|
|
71
|
+
<meta name="twitter:card" content="summary_large_image" />
|
|
72
|
+
<ThemeScript />
|
|
73
|
+
</head>
|
|
74
|
+
<body>
|
|
75
|
+
{
|
|
76
|
+
/* Outside <main> so it can span the viewport while its contents stay on
|
|
77
|
+
the same measure as the page. Sticky rather than fixed: it still takes
|
|
78
|
+
part in layout, so nothing needs a compensating top margin. */
|
|
79
|
+
}
|
|
80
|
+
<header class="site">
|
|
81
|
+
<div class="site__inner">
|
|
82
|
+
<a class="brand" href={siteUrl('/')}>
|
|
83
|
+
<img src={siteUrl('/rxova-logo-256.png')} alt="" width="28" height="28" />
|
|
84
|
+
<span>rxova</span>
|
|
85
|
+
</a>
|
|
86
|
+
{
|
|
87
|
+
/* Fixed order on every surface. It used to render the current item
|
|
88
|
+
second, so Blog and Updates swapped places as you moved between them —
|
|
89
|
+
the menu shifting under the cursor you just clicked with. */
|
|
90
|
+
}
|
|
91
|
+
<nav aria-label="Sections">
|
|
92
|
+
<a href={siteUrl('/')}>Projects</a>
|
|
93
|
+
{
|
|
94
|
+
SECTIONS.map((section) =>
|
|
95
|
+
section.path === base ? (
|
|
96
|
+
<a href={inside()} aria-current="page">
|
|
97
|
+
{section.label}
|
|
98
|
+
</a>
|
|
99
|
+
) : (
|
|
100
|
+
<a href={siteUrl(section.path)}>{section.label}</a>
|
|
101
|
+
),
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
</nav>
|
|
105
|
+
{/* In the header, not floating over it — see ThemeToggle's `floating`. */}
|
|
106
|
+
<ThemeToggle floating={false} />
|
|
107
|
+
</div>
|
|
108
|
+
</header>
|
|
109
|
+
|
|
110
|
+
<main>
|
|
111
|
+
<slot />
|
|
112
|
+
</main>
|
|
113
|
+
|
|
114
|
+
<div class="foot">
|
|
115
|
+
<SiteFooter />
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<style>
|
|
119
|
+
/* Sticky, translucent, full-bleed. It was a plain in-flow row with the
|
|
120
|
+
theme button floating over its right end, which on a phone put the
|
|
121
|
+
button on top of the nav links and left the header behind as soon as
|
|
122
|
+
you scrolled — a long post gave you no way back without scrolling to
|
|
123
|
+
the top. */
|
|
124
|
+
.site {
|
|
125
|
+
position: sticky;
|
|
126
|
+
top: 0;
|
|
127
|
+
z-index: 10;
|
|
128
|
+
border-bottom: 1px solid var(--rule);
|
|
129
|
+
background: color-mix(in srgb, var(--bg) 88%, transparent);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* The blur is what keeps text legible as it passes under the bar. Where
|
|
133
|
+
it is unsupported the background falls back to nearly opaque, which is
|
|
134
|
+
why the mix above is 88% and not 60%. */
|
|
135
|
+
@supports (backdrop-filter: blur(8px)) or (-webkit-backdrop-filter: blur(8px)) {
|
|
136
|
+
.site {
|
|
137
|
+
background: color-mix(in srgb, var(--bg) 72%, transparent);
|
|
138
|
+
-webkit-backdrop-filter: blur(10px);
|
|
139
|
+
backdrop-filter: blur(10px);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.site__inner {
|
|
144
|
+
max-width: var(--max);
|
|
145
|
+
margin: 0 auto;
|
|
146
|
+
padding: 0.75rem 1.5rem;
|
|
147
|
+
display: flex;
|
|
148
|
+
align-items: center;
|
|
149
|
+
gap: 1rem;
|
|
150
|
+
/* No wrapping: a two-line header would eat a phone's viewport, and the
|
|
151
|
+
row is sized to fit at the narrowest widths we support. */
|
|
152
|
+
min-height: 3.5rem;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* The nav takes the space between the brand and the toggle, and pushes
|
|
156
|
+
itself to the right of it. */
|
|
157
|
+
.site nav {
|
|
158
|
+
margin-left: auto;
|
|
159
|
+
}
|
|
160
|
+
.brand {
|
|
161
|
+
display: inline-flex;
|
|
162
|
+
align-items: center;
|
|
163
|
+
gap: 0.55rem;
|
|
164
|
+
color: var(--fg);
|
|
165
|
+
text-decoration: none;
|
|
166
|
+
font-weight: 640;
|
|
167
|
+
letter-spacing: -0.01em;
|
|
168
|
+
}
|
|
169
|
+
.brand img {
|
|
170
|
+
display: block;
|
|
171
|
+
border-radius: 6px;
|
|
172
|
+
}
|
|
173
|
+
.site nav {
|
|
174
|
+
display: flex;
|
|
175
|
+
gap: 1.4rem;
|
|
176
|
+
}
|
|
177
|
+
.site nav a {
|
|
178
|
+
color: var(--muted);
|
|
179
|
+
text-decoration: none;
|
|
180
|
+
font-size: 0.95rem;
|
|
181
|
+
white-space: nowrap;
|
|
182
|
+
}
|
|
183
|
+
.site nav a:hover {
|
|
184
|
+
color: var(--fg);
|
|
185
|
+
}
|
|
186
|
+
.site nav a[aria-current='page'] {
|
|
187
|
+
color: var(--fg);
|
|
188
|
+
text-decoration: underline;
|
|
189
|
+
text-underline-offset: 5px;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/* Brand mark, three links and the toggle at 390px: tight, but it fits on
|
|
193
|
+
one row, which is worth more here than the extra breathing room. */
|
|
194
|
+
@media (max-width: 34rem) {
|
|
195
|
+
.site__inner {
|
|
196
|
+
gap: 0.75rem;
|
|
197
|
+
padding-inline: 1rem;
|
|
198
|
+
min-height: 3.25rem;
|
|
199
|
+
}
|
|
200
|
+
.site nav {
|
|
201
|
+
gap: 0.9rem;
|
|
202
|
+
}
|
|
203
|
+
.site nav a {
|
|
204
|
+
font-size: 0.875rem;
|
|
205
|
+
}
|
|
206
|
+
.brand span {
|
|
207
|
+
/* The logo carries the identity in the space left; the word does not
|
|
208
|
+
fit beside three links on the narrowest phones. */
|
|
209
|
+
display: none;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
main {
|
|
214
|
+
max-width: var(--max);
|
|
215
|
+
margin: 0 auto;
|
|
216
|
+
padding: clamp(2rem, 6vh, 3.5rem) 1.5rem 4rem;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/* The footer's own rules are unscoped (footer.css, shared with the docs
|
|
220
|
+
sites); this only puts it on the page's measure. */
|
|
221
|
+
.foot {
|
|
222
|
+
max-width: var(--max);
|
|
223
|
+
margin: 0 auto;
|
|
224
|
+
padding: 0 1.5rem;
|
|
225
|
+
}
|
|
226
|
+
</style>
|
|
227
|
+
|
|
228
|
+
<!-- Prose styling for rendered markdown. Global rather than scoped: the
|
|
229
|
+
markup comes from <Content /> at build time, so it carries none of
|
|
230
|
+
Astro's scoping attributes. Kept here so both /blog and /updates read
|
|
231
|
+
the same. -->
|
|
232
|
+
<style is:global>
|
|
233
|
+
/* Global because `html` sits outside Astro's scoping. Without it a jump
|
|
234
|
+
to a heading anchor lands under the sticky header. */
|
|
235
|
+
html {
|
|
236
|
+
scroll-padding-top: 4.5rem;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.prose {
|
|
240
|
+
color: var(--muted);
|
|
241
|
+
line-height: 1.7;
|
|
242
|
+
}
|
|
243
|
+
.prose > * + * {
|
|
244
|
+
margin-top: 1.1rem;
|
|
245
|
+
}
|
|
246
|
+
.prose h2 {
|
|
247
|
+
color: var(--fg);
|
|
248
|
+
font-size: 1.25rem;
|
|
249
|
+
font-weight: 640;
|
|
250
|
+
letter-spacing: -0.01em;
|
|
251
|
+
margin-top: 2.5rem;
|
|
252
|
+
}
|
|
253
|
+
.prose h3 {
|
|
254
|
+
color: var(--fg);
|
|
255
|
+
font-size: 1.05rem;
|
|
256
|
+
font-weight: 620;
|
|
257
|
+
margin-top: 2rem;
|
|
258
|
+
}
|
|
259
|
+
.prose a {
|
|
260
|
+
color: var(--fg);
|
|
261
|
+
text-decoration: underline;
|
|
262
|
+
text-underline-offset: 2px;
|
|
263
|
+
}
|
|
264
|
+
.prose strong {
|
|
265
|
+
color: var(--fg);
|
|
266
|
+
font-weight: 620;
|
|
267
|
+
}
|
|
268
|
+
.prose ul,
|
|
269
|
+
.prose ol {
|
|
270
|
+
margin-left: 1.2rem;
|
|
271
|
+
}
|
|
272
|
+
.prose li + li {
|
|
273
|
+
margin-top: 0.35rem;
|
|
274
|
+
}
|
|
275
|
+
.prose code {
|
|
276
|
+
font-family: var(--mono);
|
|
277
|
+
font-size: 0.88em;
|
|
278
|
+
background: var(--tag-bg);
|
|
279
|
+
padding: 0.05rem 0.35rem;
|
|
280
|
+
border-radius: 4px;
|
|
281
|
+
}
|
|
282
|
+
.prose pre {
|
|
283
|
+
background: var(--card);
|
|
284
|
+
border: 1px solid var(--rule);
|
|
285
|
+
border-radius: 10px;
|
|
286
|
+
padding: 1rem 1.1rem;
|
|
287
|
+
/* Long lines scroll inside the block; the page itself never does. */
|
|
288
|
+
overflow-x: auto;
|
|
289
|
+
}
|
|
290
|
+
.prose pre code {
|
|
291
|
+
background: none;
|
|
292
|
+
padding: 0;
|
|
293
|
+
}
|
|
294
|
+
.prose blockquote {
|
|
295
|
+
border-left: 2px solid var(--rule);
|
|
296
|
+
padding-left: 1rem;
|
|
297
|
+
color: var(--faint);
|
|
298
|
+
}
|
|
299
|
+
.prose img {
|
|
300
|
+
max-width: 100%;
|
|
301
|
+
height: auto;
|
|
302
|
+
border-radius: 10px;
|
|
303
|
+
}
|
|
304
|
+
.prose table {
|
|
305
|
+
width: 100%;
|
|
306
|
+
border-collapse: collapse;
|
|
307
|
+
font-size: 0.95rem;
|
|
308
|
+
}
|
|
309
|
+
.prose th,
|
|
310
|
+
.prose td {
|
|
311
|
+
border-bottom: 1px solid var(--rule);
|
|
312
|
+
padding: 0.5rem 0.6rem;
|
|
313
|
+
text-align: left;
|
|
314
|
+
}
|
|
315
|
+
.prose th {
|
|
316
|
+
color: var(--fg);
|
|
317
|
+
font-weight: 620;
|
|
318
|
+
}
|
|
319
|
+
</style>
|
|
320
|
+
</body>
|
|
321
|
+
</html>
|
|
@@ -16,9 +16,29 @@
|
|
|
16
16
|
* Pairs with ThemeScript.astro in <head>, which is what actually avoids the flash;
|
|
17
17
|
* this only handles clicks and keeps the icon honest.
|
|
18
18
|
*/
|
|
19
|
+
|
|
20
|
+
interface Props {
|
|
21
|
+
/**
|
|
22
|
+
* Pin the button to the top-right corner of the viewport.
|
|
23
|
+
*
|
|
24
|
+
* The default, because that is where it has always been and the landing lays
|
|
25
|
+
* out around it. `SiteShell` passes `false`: it puts the button in its own
|
|
26
|
+
* sticky header, and a fixed button over a fixed header is two things
|
|
27
|
+
* competing for the same corner — on a phone the floating one landed on top
|
|
28
|
+
* of the nav.
|
|
29
|
+
*/
|
|
30
|
+
floating?: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const { floating = true } = Astro.props
|
|
19
34
|
---
|
|
20
35
|
|
|
21
|
-
<button
|
|
36
|
+
<button
|
|
37
|
+
id="theme-toggle"
|
|
38
|
+
class:list={['theme-toggle', { 'theme-toggle--floating': floating }]}
|
|
39
|
+
type="button"
|
|
40
|
+
aria-label="Toggle theme"
|
|
41
|
+
>
|
|
22
42
|
<span class="icon" aria-hidden="true"
|
|
23
43
|
><svg
|
|
24
44
|
xmlns="http://www.w3.org/2000/svg"
|
|
@@ -35,37 +55,67 @@
|
|
|
35
55
|
</button>
|
|
36
56
|
|
|
37
57
|
<style>
|
|
58
|
+
/* Quiet by default: no border, no fill, the same muted ink as the nav links
|
|
59
|
+
beside it. A bordered card-coloured box was the heaviest thing in a header
|
|
60
|
+
that is otherwise three grey words — it read as a primary control on a page
|
|
61
|
+
where changing the theme is the least important thing you can do. */
|
|
38
62
|
.theme-toggle {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
width: 2.25rem;
|
|
43
|
-
height: 2.25rem;
|
|
63
|
+
flex: none;
|
|
64
|
+
width: 2rem;
|
|
65
|
+
height: 2rem;
|
|
44
66
|
display: grid;
|
|
45
67
|
place-items: center;
|
|
46
|
-
border: 1px solid
|
|
68
|
+
border: 1px solid transparent;
|
|
47
69
|
border-radius: 8px;
|
|
48
|
-
background:
|
|
49
|
-
color: var(--
|
|
70
|
+
background: none;
|
|
71
|
+
color: var(--muted);
|
|
50
72
|
cursor: pointer;
|
|
51
|
-
z-index: 3;
|
|
52
73
|
transition:
|
|
74
|
+
color 0.15s ease,
|
|
53
75
|
border-color 0.15s ease,
|
|
54
76
|
background-color 0.2s ease;
|
|
55
77
|
}
|
|
56
78
|
|
|
79
|
+
/* Floating, it has no header to belong to and sits over arbitrary page
|
|
80
|
+
content, so it keeps the border and the fill — without them there is
|
|
81
|
+
nothing to say it is a button. */
|
|
82
|
+
.theme-toggle--floating {
|
|
83
|
+
position: fixed;
|
|
84
|
+
top: 1rem;
|
|
85
|
+
right: 1rem;
|
|
86
|
+
/* Above SiteShell's sticky header (z-index 10). Nothing pairs the two
|
|
87
|
+
today, but the whole point of a fixed button is that it stays reachable,
|
|
88
|
+
and the header would otherwise paint straight over it. */
|
|
89
|
+
z-index: 20;
|
|
90
|
+
width: 2.25rem;
|
|
91
|
+
height: 2.25rem;
|
|
92
|
+
border-color: var(--rule);
|
|
93
|
+
background: var(--card);
|
|
94
|
+
color: var(--fg);
|
|
95
|
+
}
|
|
96
|
+
|
|
57
97
|
.theme-toggle .icon {
|
|
58
98
|
display: grid;
|
|
59
99
|
place-items: center;
|
|
60
100
|
}
|
|
61
101
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
102
|
+
/* `:global` because the icon is swapped by innerHTML in the script below, and
|
|
103
|
+
what that writes carries none of Astro's scoping attributes. Scoped, these
|
|
104
|
+
rules applied only to the SVG the server rendered — the first `render()`
|
|
105
|
+
replaced it with one at Lucide's own 24px and stroke-width 2, which is why
|
|
106
|
+
the button looked so much heavier than the nav beside it. */
|
|
107
|
+
.theme-toggle :global(svg) {
|
|
108
|
+
width: 1.05rem;
|
|
109
|
+
height: 1.05rem;
|
|
65
110
|
display: block;
|
|
111
|
+
stroke-width: 1.6;
|
|
66
112
|
}
|
|
67
113
|
|
|
68
114
|
.theme-toggle:hover {
|
|
115
|
+
color: var(--fg);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.theme-toggle--floating:hover {
|
|
69
119
|
border-color: var(--faint);
|
|
70
120
|
}
|
|
71
121
|
|
package/src/footer.css
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Styles for `components/SiteFooter.astro`.
|
|
3
|
+
*
|
|
4
|
+
* Its own file because the footer is the one piece of chrome both kinds of
|
|
5
|
+
* surface render: the three Starlight docs sites and the plain Astro ones
|
|
6
|
+
* (/blog, /updates, the landing). It used to live inside starlight.css, which
|
|
7
|
+
* is why /blog and /updates shipped a dot-separated link row instead — the
|
|
8
|
+
* shared footer would have rendered unstyled there.
|
|
9
|
+
*
|
|
10
|
+
* Imported by both `starlight.css` and `astro.css`; nothing needs to import it
|
|
11
|
+
* directly. Written against `--rx-*` tokens only, so it does not depend on the
|
|
12
|
+
* short aliases astro.css defines.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
@import './tokens.css';
|
|
16
|
+
|
|
17
|
+
.rx-footer {
|
|
18
|
+
margin-top: 4rem;
|
|
19
|
+
padding: 3rem 0 2rem;
|
|
20
|
+
border-top: 1px solid var(--rx-rule);
|
|
21
|
+
color: var(--rx-muted);
|
|
22
|
+
font-size: 0.875rem;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* The brand block and the link columns, side by side where there is room.
|
|
26
|
+
`auto-fit` rather than a media query because what matters is how wide the
|
|
27
|
+
footer's *container* is, not the viewport: on /blog the page measure is 44rem
|
|
28
|
+
on any screen, and a viewport query would have put the brand block beside the
|
|
29
|
+
columns and squeezed them to two cramped tracks on a desktop. */
|
|
30
|
+
.rx-footer__top {
|
|
31
|
+
display: grid;
|
|
32
|
+
grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
|
|
33
|
+
gap: 2.5rem 3rem;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.rx-footer__brand {
|
|
37
|
+
display: flex;
|
|
38
|
+
flex-direction: column;
|
|
39
|
+
gap: 0.75rem;
|
|
40
|
+
align-items: flex-start;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.rx-footer__mark {
|
|
44
|
+
display: inline-flex;
|
|
45
|
+
align-items: center;
|
|
46
|
+
gap: 0.55rem;
|
|
47
|
+
color: var(--rx-fg);
|
|
48
|
+
font-size: 1rem;
|
|
49
|
+
font-weight: 640;
|
|
50
|
+
letter-spacing: -0.01em;
|
|
51
|
+
text-decoration: none;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.rx-footer__mark img {
|
|
55
|
+
display: block;
|
|
56
|
+
border-radius: 6px;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.rx-footer__blurb {
|
|
60
|
+
margin: 0;
|
|
61
|
+
max-width: 30ch;
|
|
62
|
+
color: var(--rx-faint);
|
|
63
|
+
line-height: 1.6;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* 8rem, not the 11rem this used to be: it is the widest track that still fits
|
|
67
|
+
two columns on a 360px phone, and one column of five-word links down the
|
|
68
|
+
whole width of a phone is a lot of scrolling for a footer. */
|
|
69
|
+
.rx-footer__columns {
|
|
70
|
+
display: grid;
|
|
71
|
+
grid-template-columns: repeat(auto-fit, minmax(min(100%, 8rem), 1fr));
|
|
72
|
+
gap: 2rem 1.5rem;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.rx-footer__title {
|
|
76
|
+
margin-bottom: 0.75rem;
|
|
77
|
+
color: var(--rx-fg);
|
|
78
|
+
font-size: 0.75rem;
|
|
79
|
+
font-weight: 700;
|
|
80
|
+
letter-spacing: 0.06em;
|
|
81
|
+
text-transform: uppercase;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.rx-footer__list {
|
|
85
|
+
display: flex;
|
|
86
|
+
flex-direction: column;
|
|
87
|
+
gap: 0.45rem;
|
|
88
|
+
margin: 0;
|
|
89
|
+
padding: 0;
|
|
90
|
+
list-style: none;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.rx-footer__list a {
|
|
94
|
+
color: var(--rx-muted);
|
|
95
|
+
text-decoration: none;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.rx-footer__list a:hover {
|
|
99
|
+
color: var(--rx-fg);
|
|
100
|
+
text-decoration: underline;
|
|
101
|
+
text-underline-offset: 2px;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* The bottom bar: copyright on the left, the legal links on the right, and one
|
|
105
|
+
column on a phone. */
|
|
106
|
+
.rx-footer__legal {
|
|
107
|
+
display: flex;
|
|
108
|
+
align-items: center;
|
|
109
|
+
justify-content: space-between;
|
|
110
|
+
flex-wrap: wrap;
|
|
111
|
+
gap: 0.5rem 1.25rem;
|
|
112
|
+
margin-top: 2.5rem;
|
|
113
|
+
padding-top: 1.25rem;
|
|
114
|
+
border-top: 1px solid var(--rx-rule);
|
|
115
|
+
color: var(--rx-faint);
|
|
116
|
+
font-size: 0.8125rem;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.rx-footer__legal p {
|
|
120
|
+
margin: 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.rx-footer__legal-links {
|
|
124
|
+
display: flex;
|
|
125
|
+
align-items: center;
|
|
126
|
+
gap: 1.25rem;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.rx-footer__legal a {
|
|
130
|
+
color: var(--rx-faint);
|
|
131
|
+
text-decoration: none;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.rx-footer__legal a:hover {
|
|
135
|
+
color: var(--rx-fg);
|
|
136
|
+
text-decoration: underline;
|
|
137
|
+
text-underline-offset: 2px;
|
|
138
|
+
}
|
package/src/sites.ts
CHANGED
|
@@ -102,7 +102,7 @@ export const REPOS = [
|
|
|
102
102
|
|
|
103
103
|
export type RepoId = (typeof REPOS)[number]['id']
|
|
104
104
|
|
|
105
|
-
/** The ids only, for schema validation — see packages/schemas. */
|
|
105
|
+
/** The ids only, for schema validation — see packages/website-schemas. */
|
|
106
106
|
export const REPO_IDS: readonly RepoId[] = REPOS.map((r) => r.id)
|
|
107
107
|
|
|
108
108
|
export function getRepo(id: RepoId): (typeof REPOS)[number] {
|
package/src/starlight.css
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
@import './tokens.css';
|
|
19
|
+
@import './footer.css';
|
|
19
20
|
|
|
20
21
|
:root {
|
|
21
22
|
/* Accent. */
|
|
@@ -131,53 +132,6 @@ header.header {
|
|
|
131
132
|
|
|
132
133
|
/* --- Footer -------------------------------------------------------------- */
|
|
133
134
|
|
|
134
|
-
.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
border-top: 1px solid var(--rx-rule);
|
|
138
|
-
color: var(--rx-muted);
|
|
139
|
-
font-size: 0.875rem;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
.rx-footer__columns {
|
|
143
|
-
display: grid;
|
|
144
|
-
grid-template-columns: repeat(auto-fit, minmax(min(100%, 11rem), 1fr));
|
|
145
|
-
gap: 2rem;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
.rx-footer__title {
|
|
149
|
-
margin-bottom: 0.75rem;
|
|
150
|
-
color: var(--rx-fg);
|
|
151
|
-
font-size: 0.75rem;
|
|
152
|
-
font-weight: 700;
|
|
153
|
-
letter-spacing: 0.06em;
|
|
154
|
-
text-transform: uppercase;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
.rx-footer__list {
|
|
158
|
-
display: flex;
|
|
159
|
-
flex-direction: column;
|
|
160
|
-
gap: 0.45rem;
|
|
161
|
-
margin: 0;
|
|
162
|
-
padding: 0;
|
|
163
|
-
list-style: none;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
.rx-footer__list a {
|
|
167
|
-
color: var(--rx-muted);
|
|
168
|
-
text-decoration: none;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
.rx-footer__list a:hover {
|
|
172
|
-
color: var(--rx-fg);
|
|
173
|
-
text-decoration: underline;
|
|
174
|
-
text-underline-offset: 2px;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
.rx-footer__legal {
|
|
178
|
-
margin-top: 2.5rem;
|
|
179
|
-
padding-top: 1.25rem;
|
|
180
|
-
border-top: 1px solid var(--rx-rule);
|
|
181
|
-
color: var(--rx-faint);
|
|
182
|
-
font-size: 0.8125rem;
|
|
183
|
-
}
|
|
135
|
+
/* Its styles are in footer.css, imported at the top of this file: /blog and
|
|
136
|
+
/updates render the same footer and load astro.css, not this one, so the
|
|
137
|
+
rules cannot live here. */
|