@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rxova/brand",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "description": "Design tokens, Starlight theme and shared site chrome for rxova.org",
6
6
  "keywords": [
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 four-column rxova footer, shared by all four surfaces.
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-footer__columns">
29
- {
30
- docs && docs.length > 0 && (
31
- <div>
32
- <h2 class="rx-footer__title">Docs</h2>
33
- <ul class="rx-footer__list">
34
- {docs.map((link) => (
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={link.href}>{link.label}</a>
69
+ <a href={`${RXOVA_ORIGIN}${p.mount}`}>{p.label}</a>
37
70
  </li>
38
- ))}
39
- </ul>
40
- </div>
41
- )
42
- }
71
+ ))
72
+ }
73
+ </ul>
74
+ </div>
43
75
 
44
- <div>
45
- <h2 class="rx-footer__title">Projects</h2>
46
- <ul class="rx-footer__list">
47
- <li><a href={RXOVA_ORIGIN}>rxova.org</a></li>
48
- {
49
- PROJECTS.map((p) => (
50
- <li>
51
- <a href={`${RXOVA_ORIGIN}${p.mount}`}>{p.label}</a>
52
- </li>
53
- ))
54
- }
55
- </ul>
56
- </div>
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
- self && (
60
- <div>
61
- <h2 class="rx-footer__title">Community</h2>
62
- <ul class="rx-footer__list">
63
- <li>
64
- <a href={self.repo}>GitHub</a>
65
- </li>
66
- <li>
67
- <a href={`${self.repo}/issues`}>Issues</a>
68
- </li>
69
- <li>
70
- <a href={`${self.repo}/discussions`}>Discussions</a>
71
- </li>
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
- <div>
84
- <h2 class="rx-footer__title">rxova</h2>
85
- <ul class="rx-footer__list">
86
- <li><a href={RXOVA_ORIGIN}>Home</a></li>
87
- <li><a href="https://github.com/rxova">GitHub org</a></li>
88
- <li><a href={siteUrl('/privacy')}>Privacy</a></li>
89
- <li><a href={siteUrl('/terms')}>Terms</a></li>
90
- <li><a href="mailto:rxova@proton.me">rxova@proton.me</a></li>
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
- <p class="rx-footer__legal">© {year} rxova · MIT licensed</p>
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
- <ThemeToggle />
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
- <style>
121
- .site {
122
- display: flex;
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
- margin-top: clamp(3rem, 8vh, 5rem);
170
- padding-top: 1.75rem;
171
- border-top: 1px solid var(--rule);
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 id="theme-toggle" class="theme-toggle" type="button" aria-label="Toggle theme">
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
- position: fixed;
40
- top: 1rem;
41
- right: 1rem;
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 var(--rule);
68
+ border: 1px solid transparent;
47
69
  border-radius: 8px;
48
- background: var(--card);
49
- color: var(--fg);
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
- .theme-toggle svg {
63
- width: 1.15rem;
64
- height: 1.15rem;
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
- .rx-footer {
135
- margin-top: 4rem;
136
- padding: 2.5rem 0 2rem;
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. */