@rxova/brand 0.5.0 → 0.7.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/Header.astro +174 -0
- package/src/components/SiteFooter.astro +99 -61
- package/src/components/SiteShell.astro +30 -103
- package/src/components/ThemeToggle.astro +63 -13
- package/src/footer.css +138 -0
- 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);
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* The rxova.org header — the one every plain-Astro surface renders.
|
|
4
|
+
*
|
|
5
|
+
* There used to be two copies of this markup and its styles: one inside
|
|
6
|
+
* `SiteShell` (for /blog and /updates) and a hand-rolled twin in the website
|
|
7
|
+
* repo (for the landing at `/`). They drifted — the landing missed the sticky
|
|
8
|
+
* update the shell got — which is exactly the failure mode of a header that
|
|
9
|
+
* lives in more than one place. This is the single copy; `SiteShell` renders it,
|
|
10
|
+
* and the website imports it over npm and renders it too.
|
|
11
|
+
*
|
|
12
|
+
* It takes the menu as data rather than owning it. The two surfaces resolve their
|
|
13
|
+
* URLs differently — the shell links absolutely through `siteUrl` because /blog
|
|
14
|
+
* and /updates are mounted at their own base paths, while the landing sits at the
|
|
15
|
+
* root and links relatively — and the landing's menu is derived from what is
|
|
16
|
+
* actually deployed. So each caller passes a fully-resolved list; this owns how
|
|
17
|
+
* the header *looks and behaves*, which is the part that was drifting.
|
|
18
|
+
*
|
|
19
|
+
* Not for the docs sites at /packages/*: those are Starlight and take their
|
|
20
|
+
* chrome from `sharedStarlightConfig`, not from an Astro component.
|
|
21
|
+
*/
|
|
22
|
+
import ThemeToggle from './ThemeToggle.astro'
|
|
23
|
+
|
|
24
|
+
export interface HeaderItem {
|
|
25
|
+
label: string
|
|
26
|
+
/** Fully resolved by the caller — absolute or root-relative as its base needs. */
|
|
27
|
+
href: string
|
|
28
|
+
/** Marks the item for the surface the reader is on. */
|
|
29
|
+
current?: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface Props {
|
|
33
|
+
/** Where the brand mark links — the site root, resolved by the caller. */
|
|
34
|
+
homeHref: string
|
|
35
|
+
/** Logo `src`, resolved by the caller for its base. */
|
|
36
|
+
logoSrc: string
|
|
37
|
+
/** The menu, in the one fixed order every surface shows it. */
|
|
38
|
+
items: readonly HeaderItem[]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const { homeHref, logoSrc, items } = Astro.props
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
{
|
|
45
|
+
/* Outside <main> in every shell so it can span the viewport while its contents
|
|
46
|
+
stay on the page's measure. Sticky rather than fixed: it still takes part in
|
|
47
|
+
layout, so nothing below needs a compensating top margin. */
|
|
48
|
+
}
|
|
49
|
+
<header class="site">
|
|
50
|
+
<div class="site__inner">
|
|
51
|
+
<a class="brand" href={homeHref}>
|
|
52
|
+
<img src={logoSrc} alt="" width="28" height="28" />
|
|
53
|
+
<span>rxova</span>
|
|
54
|
+
</a>
|
|
55
|
+
<nav aria-label="Sections">
|
|
56
|
+
{
|
|
57
|
+
items.map((item) => (
|
|
58
|
+
<a href={item.href} aria-current={item.current ? 'page' : undefined}>
|
|
59
|
+
{item.label}
|
|
60
|
+
</a>
|
|
61
|
+
))
|
|
62
|
+
}
|
|
63
|
+
</nav>
|
|
64
|
+
{/* In the header, not floating over it — see ThemeToggle's `floating`. */}
|
|
65
|
+
<ThemeToggle floating={false} />
|
|
66
|
+
</div>
|
|
67
|
+
</header>
|
|
68
|
+
|
|
69
|
+
<style>
|
|
70
|
+
/* Sticky, translucent, full-bleed. It was a plain in-flow row with the theme
|
|
71
|
+
button floating over its right end, which on a phone put the button on top
|
|
72
|
+
of the nav links and left the header behind as soon as you scrolled — a long
|
|
73
|
+
post gave you no way back without scrolling to the top. */
|
|
74
|
+
.site {
|
|
75
|
+
position: sticky;
|
|
76
|
+
top: 0;
|
|
77
|
+
z-index: 10;
|
|
78
|
+
border-bottom: 1px solid var(--rule);
|
|
79
|
+
background: color-mix(in srgb, var(--bg) 88%, transparent);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* The blur is what keeps text legible as it passes under the bar. Where it is
|
|
83
|
+
unsupported the background falls back to nearly opaque, which is why the mix
|
|
84
|
+
above is 88% and not 60%. */
|
|
85
|
+
@supports (backdrop-filter: blur(8px)) or (-webkit-backdrop-filter: blur(8px)) {
|
|
86
|
+
.site {
|
|
87
|
+
background: color-mix(in srgb, var(--bg) 72%, transparent);
|
|
88
|
+
-webkit-backdrop-filter: blur(10px);
|
|
89
|
+
backdrop-filter: blur(10px);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.site__inner {
|
|
94
|
+
max-width: var(--max);
|
|
95
|
+
margin: 0 auto;
|
|
96
|
+
padding: 0.75rem 1.5rem;
|
|
97
|
+
display: flex;
|
|
98
|
+
align-items: center;
|
|
99
|
+
gap: 1rem;
|
|
100
|
+
/* No wrapping: a two-line header would eat a phone's viewport, and the row
|
|
101
|
+
is sized to fit at the narrowest widths we support. */
|
|
102
|
+
min-height: 3.5rem;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.brand {
|
|
106
|
+
display: inline-flex;
|
|
107
|
+
align-items: center;
|
|
108
|
+
gap: 0.55rem;
|
|
109
|
+
color: var(--fg);
|
|
110
|
+
text-decoration: none;
|
|
111
|
+
font-weight: 640;
|
|
112
|
+
letter-spacing: -0.01em;
|
|
113
|
+
}
|
|
114
|
+
.brand img {
|
|
115
|
+
display: block;
|
|
116
|
+
border-radius: 6px;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* The nav takes the space between the brand and the toggle, and pushes itself
|
|
120
|
+
to the right of it. */
|
|
121
|
+
nav {
|
|
122
|
+
margin-left: auto;
|
|
123
|
+
display: flex;
|
|
124
|
+
gap: 1.4rem;
|
|
125
|
+
}
|
|
126
|
+
nav a {
|
|
127
|
+
color: var(--muted);
|
|
128
|
+
text-decoration: none;
|
|
129
|
+
font-size: 0.95rem;
|
|
130
|
+
white-space: nowrap;
|
|
131
|
+
}
|
|
132
|
+
nav a:hover {
|
|
133
|
+
color: var(--fg);
|
|
134
|
+
}
|
|
135
|
+
/* The current section is marked for everyone, not only for assistive tech —
|
|
136
|
+
aria-current on its own is invisible. */
|
|
137
|
+
nav a[aria-current='page'] {
|
|
138
|
+
color: var(--fg);
|
|
139
|
+
text-decoration: underline;
|
|
140
|
+
text-underline-offset: 5px;
|
|
141
|
+
}
|
|
142
|
+
nav a:focus-visible {
|
|
143
|
+
outline: 2px solid var(--accent);
|
|
144
|
+
outline-offset: 3px;
|
|
145
|
+
border-radius: 2px;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* Brand mark, the links and the toggle stay on one row at the narrowest phones
|
|
149
|
+
by dropping the wordmark — the logo carries the identity in the space left. */
|
|
150
|
+
@media (max-width: 34rem) {
|
|
151
|
+
.site__inner {
|
|
152
|
+
gap: 0.75rem;
|
|
153
|
+
padding-inline: 1rem;
|
|
154
|
+
min-height: 3.25rem;
|
|
155
|
+
}
|
|
156
|
+
nav {
|
|
157
|
+
gap: 0.9rem;
|
|
158
|
+
}
|
|
159
|
+
nav a {
|
|
160
|
+
font-size: 0.875rem;
|
|
161
|
+
}
|
|
162
|
+
.brand span {
|
|
163
|
+
display: none;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
</style>
|
|
167
|
+
|
|
168
|
+
<!-- Global because `html` sits outside Astro's scoping. Without it a jump to an
|
|
169
|
+
in-page anchor would land under the sticky header. -->
|
|
170
|
+
<style is:global>
|
|
171
|
+
html {
|
|
172
|
+
scroll-padding-top: 4.5rem;
|
|
173
|
+
}
|
|
174
|
+
</style>
|
|
@@ -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>
|
|
@@ -14,8 +14,9 @@ import '../astro.css'
|
|
|
14
14
|
|
|
15
15
|
import { RXOVA_ORIGIN, siteUrl } from '../sites.ts'
|
|
16
16
|
|
|
17
|
+
import Header, { type HeaderItem } from './Header.astro'
|
|
18
|
+
import SiteFooter from './SiteFooter.astro'
|
|
17
19
|
import ThemeScript from './ThemeScript.astro'
|
|
18
|
-
import ThemeToggle from './ThemeToggle.astro'
|
|
19
20
|
|
|
20
21
|
interface Props {
|
|
21
22
|
title: string
|
|
@@ -27,7 +28,6 @@ interface Props {
|
|
|
27
28
|
|
|
28
29
|
const { title, description, path, ogType = 'website' } = Astro.props
|
|
29
30
|
const canonical = `${RXOVA_ORIGIN}${path}`
|
|
30
|
-
const year = new Date().getFullYear()
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* The menu, in one fixed order for every surface.
|
|
@@ -51,6 +51,21 @@ const SECTIONS = [
|
|
|
51
51
|
* against the mount and point at `/blog/updates`.
|
|
52
52
|
*/
|
|
53
53
|
const inside = (p = '') => `${base}/${p}`.replace(/\/{2,}/g, '/')
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The menu, fully resolved for this surface, in the one fixed order every surface
|
|
57
|
+
* shows it. The current section links inside its own base; every other link is
|
|
58
|
+
* absolute so it does not resolve against this mount. `Header` owns how it looks;
|
|
59
|
+
* this owns where it points.
|
|
60
|
+
*/
|
|
61
|
+
const items: HeaderItem[] = [
|
|
62
|
+
{ label: 'Projects', href: siteUrl('/') },
|
|
63
|
+
...SECTIONS.map((section) =>
|
|
64
|
+
section.path === base
|
|
65
|
+
? { label: section.label, href: inside(), current: true }
|
|
66
|
+
: { label: section.label, href: siteUrl(section.path) },
|
|
67
|
+
),
|
|
68
|
+
]
|
|
54
69
|
---
|
|
55
70
|
|
|
56
71
|
<!doctype html>
|
|
@@ -72,119 +87,31 @@ const inside = (p = '') => `${base}/${p}`.replace(/\/{2,}/g, '/')
|
|
|
72
87
|
<ThemeScript />
|
|
73
88
|
</head>
|
|
74
89
|
<body>
|
|
75
|
-
<
|
|
76
|
-
<main>
|
|
77
|
-
<header class="site">
|
|
78
|
-
<a class="brand" href={siteUrl('/')}>
|
|
79
|
-
<img src={siteUrl('/rxova-logo-256.png')} alt="" width="28" height="28" />
|
|
80
|
-
<span>rxova</span>
|
|
81
|
-
</a>
|
|
82
|
-
{
|
|
83
|
-
/* Fixed order on every surface. It used to render the current item
|
|
84
|
-
second, so Blog and Updates swapped places as you moved between them —
|
|
85
|
-
the menu shifting under the cursor you just clicked with. */
|
|
86
|
-
}
|
|
87
|
-
<nav aria-label="Sections">
|
|
88
|
-
<a href={siteUrl('/')}>Projects</a>
|
|
89
|
-
{
|
|
90
|
-
SECTIONS.map((section) =>
|
|
91
|
-
section.path === base ? (
|
|
92
|
-
<a href={inside()} aria-current="page">
|
|
93
|
-
{section.label}
|
|
94
|
-
</a>
|
|
95
|
-
) : (
|
|
96
|
-
<a href={siteUrl(section.path)}>{section.label}</a>
|
|
97
|
-
),
|
|
98
|
-
)
|
|
99
|
-
}
|
|
100
|
-
</nav>
|
|
101
|
-
</header>
|
|
90
|
+
<Header homeHref={siteUrl('/')} logoSrc={siteUrl('/rxova-logo-256.png')} items={items} />
|
|
102
91
|
|
|
92
|
+
<main>
|
|
103
93
|
<slot />
|
|
104
|
-
|
|
105
|
-
<footer class="foot">
|
|
106
|
-
<span>© {year} rxova</span>
|
|
107
|
-
<span class="dot" aria-hidden="true">·</span>
|
|
108
|
-
<a href={siteUrl('/')}>Projects</a>
|
|
109
|
-
<span class="dot" aria-hidden="true">·</span>
|
|
110
|
-
<a href={siteUrl('/blog')}>Blog</a>
|
|
111
|
-
<span class="dot" aria-hidden="true">·</span>
|
|
112
|
-
<a href={siteUrl('/updates')}>Updates</a>
|
|
113
|
-
<span class="dot" aria-hidden="true">·</span>
|
|
114
|
-
<a href={siteUrl('/privacy')}>Privacy</a>
|
|
115
|
-
<span class="dot" aria-hidden="true">·</span>
|
|
116
|
-
<a href={siteUrl('/terms')}>Terms</a>
|
|
117
|
-
</footer>
|
|
118
94
|
</main>
|
|
119
95
|
|
|
120
|
-
<
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
align-items: center;
|
|
124
|
-
justify-content: space-between;
|
|
125
|
-
gap: 1rem;
|
|
126
|
-
flex-wrap: wrap;
|
|
127
|
-
padding-bottom: 1.25rem;
|
|
128
|
-
margin-bottom: clamp(2rem, 6vh, 3rem);
|
|
129
|
-
border-bottom: 1px solid var(--rule);
|
|
130
|
-
}
|
|
131
|
-
.brand {
|
|
132
|
-
display: inline-flex;
|
|
133
|
-
align-items: center;
|
|
134
|
-
gap: 0.55rem;
|
|
135
|
-
color: var(--fg);
|
|
136
|
-
text-decoration: none;
|
|
137
|
-
font-weight: 640;
|
|
138
|
-
letter-spacing: -0.01em;
|
|
139
|
-
}
|
|
140
|
-
.brand img {
|
|
141
|
-
display: block;
|
|
142
|
-
border-radius: 6px;
|
|
143
|
-
}
|
|
144
|
-
.site nav {
|
|
145
|
-
display: flex;
|
|
146
|
-
gap: 1.4rem;
|
|
147
|
-
}
|
|
148
|
-
.site nav a {
|
|
149
|
-
color: var(--muted);
|
|
150
|
-
text-decoration: none;
|
|
151
|
-
font-size: 0.95rem;
|
|
152
|
-
}
|
|
153
|
-
.site nav a:hover {
|
|
154
|
-
color: var(--fg);
|
|
155
|
-
}
|
|
156
|
-
.site nav a[aria-current='page'] {
|
|
157
|
-
color: var(--fg);
|
|
158
|
-
text-decoration: underline;
|
|
159
|
-
text-underline-offset: 5px;
|
|
160
|
-
}
|
|
96
|
+
<div class="foot">
|
|
97
|
+
<SiteFooter />
|
|
98
|
+
</div>
|
|
161
99
|
|
|
100
|
+
<style>
|
|
101
|
+
/* The header is `Header` (its own component, shared with the landing); the
|
|
102
|
+
only chrome styled here is the page body and the footer's measure. */
|
|
162
103
|
main {
|
|
163
104
|
max-width: var(--max);
|
|
164
105
|
margin: 0 auto;
|
|
165
106
|
padding: clamp(2rem, 6vh, 3.5rem) 1.5rem 4rem;
|
|
166
107
|
}
|
|
167
108
|
|
|
109
|
+
/* The footer's own rules are unscoped (footer.css, shared with the docs
|
|
110
|
+
sites); this only puts it on the page's measure. */
|
|
168
111
|
.foot {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
color: var(--faint);
|
|
173
|
-
font-size: 0.9rem;
|
|
174
|
-
display: flex;
|
|
175
|
-
align-items: center;
|
|
176
|
-
gap: 0.6rem;
|
|
177
|
-
flex-wrap: wrap;
|
|
178
|
-
}
|
|
179
|
-
.foot a {
|
|
180
|
-
color: var(--muted);
|
|
181
|
-
text-decoration: none;
|
|
182
|
-
}
|
|
183
|
-
.foot a:hover {
|
|
184
|
-
color: var(--fg);
|
|
185
|
-
}
|
|
186
|
-
.dot {
|
|
187
|
-
color: var(--rule);
|
|
112
|
+
max-width: var(--max);
|
|
113
|
+
margin: 0 auto;
|
|
114
|
+
padding: 0 1.5rem;
|
|
188
115
|
}
|
|
189
116
|
</style>
|
|
190
117
|
|
|
@@ -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/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. */
|