@rxova/brand 0.8.2 → 0.11.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/README.md CHANGED
@@ -69,20 +69,27 @@ import { PROJECTS } from '@rxova/brand'
69
69
  ---
70
70
  ```
71
71
 
72
- `astro.css` pulls in `tokens.css` and adds the reset and base element styles
73
- that Starlight gives docs sites for free.
72
+ `astro.css` pulls in `chrome.css` the tokens, the footer's styles and the
73
+ short aliases and adds the reset and base element styles that Starlight gives
74
+ docs sites for free.
75
+
76
+ Use `chrome.css` on its own where something else already owns the document, such
77
+ as rxova-website's shell templates: their body is composed into a built
78
+ Starlight page, and the reset in `astro.css` is unlayered, so it would outrank
79
+ everything in `@layer starlight.*` and flatten that page.
74
80
 
75
81
  ## What's in it
76
82
 
77
- | Export | What it is |
78
- | --------------------------------- | --------------------------------------------------------------------- |
79
- | `@rxova/brand` | `PROJECTS`, `docsUrl()`, `siteUrl()`, `sharedStarlightConfig()` |
80
- | `@rxova/brand/tokens.css` | The `--rx-*` custom properties. Everything derives from these. |
81
- | `@rxova/brand/starlight.css` | For docs sites: maps `--rx-*` onto Starlight's `--sl-*`, plus chrome |
82
- | `@rxova/brand/astro.css` | For plain Astro sites: tokens, a reset, and base element styling |
83
- | `@rxova/brand/fonts.css` | Self-hosted Space Grotesk + IBM Plex Mono |
84
- | `@rxova/brand/components/*.astro` | `SiteTitle`, `SocialIcons`, `Footer`, `SiteFooter`, `ProjectSwitcher` |
85
- | `@rxova/brand/assets/*` | The mark, logos and per-project OG images |
83
+ | Export | What it is |
84
+ | --------------------------------- | ---------------------------------------------------------------------- |
85
+ | `@rxova/brand` | `PROJECTS`, `docsUrl()`, `siteUrl()`, `sharedStarlightConfig()` |
86
+ | `@rxova/brand/tokens.css` | The `--rx-*` custom properties. Everything derives from these. |
87
+ | `@rxova/brand/starlight.css` | For docs sites: maps `--rx-*` onto Starlight's `--sl-*`, plus chrome |
88
+ | `@rxova/brand/astro.css` | For plain Astro sites: `chrome.css`, a reset, and base element styling |
89
+ | `@rxova/brand/chrome.css` | What the shared header and footer need, with nothing document-level |
90
+ | `@rxova/brand/fonts.css` | Self-hosted Space Grotesk + IBM Plex Mono |
91
+ | `@rxova/brand/components/*.astro` | `SiteTitle`, `SocialIcons`, `Footer`, `SiteFooter`, `ProjectSwitcher` |
92
+ | `@rxova/brand/assets/*` | The mark, logos and per-project OG images |
86
93
 
87
94
  ## The palette
88
95
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rxova/brand",
3
- "version": "0.8.2",
3
+ "version": "0.11.0",
4
4
  "type": "module",
5
5
  "description": "Design tokens, Starlight theme and shared site chrome for rxova.org",
6
6
  "keywords": [
@@ -33,6 +33,7 @@
33
33
  ".": "./src/index.ts",
34
34
  "./tokens.css": "./src/tokens.css",
35
35
  "./astro.css": "./src/astro.css",
36
+ "./chrome.css": "./src/chrome.css",
36
37
  "./starlight.css": "./src/starlight.css",
37
38
  "./fonts.css": "./src/fonts.css",
38
39
  "./components/*": "./src/components/*",
package/src/astro.css CHANGED
@@ -1,42 +1,21 @@
1
1
  /**
2
- * Entry point for plain Astro surfaces today, the rxova.org landing.
2
+ * Entry point for plain Astro surfaces that own their whole document — the
3
+ * rxova.org landing, /blog and /updates.
3
4
  *
4
5
  * Starlight sites use `starlight.css` instead; it maps the same tokens onto
5
- * Starlight's own variables. This file carries what a bare Astro page needs and
6
+ * Starlight's own variables. This file is `chrome.css` the tokens, the footer
7
+ * and the short aliases — plus the two things a bare Astro page needs and
6
8
  * Starlight already provides: a reset and base element styling.
7
9
  *
8
- * It also aliases the short token names the landing was written against
9
- * (`--bg`, `--fg`, `--rule`, …) onto their `--rx-*` equivalents, so adopting
10
- * this package did not require rewriting several hundred lines of scoped
11
- * component styles in one go. New work should use `--rx-*` directly; the
12
- * aliases exist to make the migration incremental, not to be a second API.
10
+ * Load this only where nothing else owns the document. A surface that renders
11
+ * the shared chrome *into* someone else's document rxova-website's shell
12
+ * templates, whose body is composed into a built Starlight page — must load
13
+ * `chrome.css` instead. The reset below is unlayered, so inside a Starlight
14
+ * document it would outrank every rule in `@layer starlight.*` and flatten the
15
+ * page; see chrome.css.
13
16
  */
14
17
 
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';
20
-
21
- :root {
22
- --bg: var(--rx-bg);
23
- --fg: var(--rx-fg);
24
- --muted: var(--rx-muted);
25
- --faint: var(--rx-faint);
26
- --rule: var(--rx-rule);
27
- --card: var(--rx-card);
28
- --tag-bg: var(--rx-tag-bg);
29
- --glow-tint: var(--rx-glow-tint);
30
- --font: var(--rx-font-sans);
31
- --mono: var(--rx-font-mono);
32
- --max: var(--rx-max);
33
-
34
- /* The landing's `--accent` is the foreground, not the brand violet: its
35
- design is deliberately monochrome and uses the gradient only as an accent
36
- rule. Mapping this to --rx-primary would tint every focus ring on the
37
- site. */
38
- --accent: var(--rx-fg);
39
- }
18
+ @import './chrome.css';
40
19
 
41
20
  /* --- Reset ---------------------------------------------------------------- */
42
21
 
package/src/chrome.css ADDED
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Everything the shared chrome needs, and nothing that touches the document.
3
+ *
4
+ * `Header`, `SiteFooter` and `ThemeToggle` are rendered on two kinds of page:
5
+ * plain Astro surfaces that own their whole document (the landing, /blog,
6
+ * /updates — see astro.css), and rxova-website's shell templates, whose body is
7
+ * composed into an already-complete Starlight document at deploy time.
8
+ *
9
+ * The second case is why this file exists. It carries the tokens, the footer's
10
+ * styles and the short aliases the chrome's scoped CSS is written against — but
11
+ * no reset and no base element styling. Those belong to whoever owns the
12
+ * document, and a shell that brings its own into a Starlight page destroys that
13
+ * page: Starlight puts all of its CSS in `@layer starlight.*`, and an unlayered
14
+ * `* { margin: 0; padding: 0 }` arriving from the shell beats every layered rule
15
+ * it has, whatever the specificity. That is not a hypothetical — it flattened
16
+ * every /packages/* docs page on rxova.org.
17
+ *
18
+ * So: load this to render the chrome, and astro.css (which is this plus the
19
+ * reset) to own a document.
20
+ */
21
+
22
+ @import './tokens.css';
23
+ /* The shared site footer, which /blog, /updates and the landing all render. It
24
+ is the one piece of chrome common to these surfaces and the Starlight ones,
25
+ so its rules sit in their own file and both entry points pull them in. */
26
+ @import './footer.css';
27
+
28
+ /* The short token names the landing and the shared chrome were written against
29
+ (`--bg`, `--fg`, `--rule`, …), aliased onto their `--rx-*` equivalents, so
30
+ adopting this package did not require rewriting several hundred lines of
31
+ scoped component styles in one go. New work should use `--rx-*` directly; the
32
+ aliases exist to make the migration incremental, not to be a second API. */
33
+ :root {
34
+ --bg: var(--rx-bg);
35
+ --fg: var(--rx-fg);
36
+ --muted: var(--rx-muted);
37
+ --faint: var(--rx-faint);
38
+ --rule: var(--rx-rule);
39
+ --card: var(--rx-card);
40
+ --tag-bg: var(--rx-tag-bg);
41
+ --glow-tint: var(--rx-glow-tint);
42
+ --font: var(--rx-font-sans);
43
+ --mono: var(--rx-font-mono);
44
+ --max: var(--rx-max);
45
+
46
+ /* The landing's `--accent` is the foreground, not the brand violet: its
47
+ design is deliberately monochrome and uses the gradient only as an accent
48
+ rule. Mapping this to --rx-primary would tint every focus ring on the
49
+ site. */
50
+ --accent: var(--rx-fg);
51
+ }
@@ -1,83 +1,35 @@
1
1
  ---
2
2
  /**
3
- * The shell for rxova.org's non-Starlight surfaces — /blog and /updates.
3
+ * Artifact document for rxova.org's non-Starlight page components.
4
4
  *
5
5
  * It lived in both packages as a 244-line file that differed on four lines, which
6
6
  * is exactly how the menu came to render in a different order on each: a fix has to
7
7
  * be made twice, and once was enough to forget.
8
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.
9
+ * This deliberately owns no site header, footer, analytics or theme bootstrap.
10
+ * It gives an independently-built page component a standards-compliant document
11
+ * for its metadata and assets; rxova-website consumes its body and supplies the
12
+ * public shell at deploy time.
11
13
  */
12
14
  import '../fonts.css'
13
15
  import '../astro.css'
14
16
 
15
- import { RXOVA_ORIGIN, siteUrl } from '../sites.ts'
16
-
17
- import Header, { type HeaderItem } from './Header.astro'
18
- import SiteFooter from './SiteFooter.astro'
19
- import ThemeScript from './ThemeScript.astro'
17
+ import { canonicalUrl, siteUrl } from '../sites.ts'
20
18
 
21
19
  interface Props {
22
20
  title: string
23
21
  description: string
24
- /** Canonical path, e.g. `/blog/why-rxova-has-a-blog`. */
22
+ /** Canonical path, e.g. `/blog/why-rxova-has-a-blog`. Normalised below. */
25
23
  path: string
26
24
  ogType?: 'website' | 'article'
27
25
  }
28
26
 
29
27
  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
- /**
43
- * The surfaces that can be the *current* one here — the mounted ones this shell
44
- * is ever built for. /about is not among them: it is a page of the landing's own
45
- * Astro build rather than a mount, so it can never match `base`, and it is added
46
- * as a fixed trailing item below.
47
- */
48
- const SECTIONS = [
49
- { label: 'Blog', path: '/blog' },
50
- { label: 'Updates', path: '/updates' },
51
- ]
52
-
53
- /**
54
- * A URL inside this surface.
55
- *
56
- * Anything *leaving* it goes through `siteUrl`: a bare `/updates` would resolve
57
- * against the mount and point at `/blog/updates`.
58
- */
59
- const inside = (p = '') => `${base}/${p}`.replace(/\/{2,}/g, '/')
60
28
 
61
- /**
62
- * The menu, fully resolved for this surface, in the one fixed order every surface
63
- * shows it. The current section links inside its own base; every other link is
64
- * absolute so it does not resolve against this mount. `Header` owns how it looks;
65
- * this owns where it points.
66
- */
67
- const items: HeaderItem[] = [
68
- { label: 'Projects', href: siteUrl('/') },
69
- ...SECTIONS.map((section) =>
70
- section.path === base
71
- ? { label: section.label, href: inside(), current: true }
72
- : { label: section.label, href: siteUrl(section.path) },
73
- ),
74
- // Fixed trailing item, like Projects is a fixed leading one, and for the same
75
- // reason: it belongs to the landing's build, so it is always deployed and can
76
- // never be the current surface here. The landing grew an About entry and these
77
- // two did not, which left a reader on a post with no way to reach it — the
78
- // failure mode this shell exists to prevent.
79
- { label: 'About', href: siteUrl('/about') },
80
- ]
29
+ // Normalised here rather than at the four call sites: every one of them had
30
+ // dropped the trailing slash, so every blog and updates page was declaring itself
31
+ // canonical at a URL that 301s. A caller can forget again; this cannot.
32
+ const canonical = canonicalUrl(path)
81
33
  ---
82
34
 
83
35
  <!doctype html>
@@ -85,8 +37,6 @@ const items: HeaderItem[] = [
85
37
  <head>
86
38
  <meta charset="utf-8" />
87
39
  <meta name="viewport" content="width=device-width, initial-scale=1" />
88
- <link rel="icon" type="image/png" href={siteUrl('/rxova-logo-256.png')} />
89
- <link rel="apple-touch-icon" href={siteUrl('/rxova-logo-256.png')} />
90
40
  <link rel="canonical" href={canonical} />
91
41
  <title>{title}</title>
92
42
  <meta name="description" content={description} />
@@ -96,35 +46,18 @@ const items: HeaderItem[] = [
96
46
  <meta property="og:url" content={canonical} />
97
47
  <meta property="og:image" content={siteUrl('/og/rxova.png')} />
98
48
  <meta name="twitter:card" content="summary_large_image" />
99
- <ThemeScript />
100
49
  </head>
101
50
  <body>
102
- <Header homeHref={siteUrl('/')} logoSrc={siteUrl('/rxova-logo-256.png')} items={items} />
103
-
104
51
  <main>
105
52
  <slot />
106
53
  </main>
107
54
 
108
- <div class="foot">
109
- <SiteFooter />
110
- </div>
111
-
112
55
  <style>
113
- /* The header is `Header` (its own component, shared with the landing); the
114
- only chrome styled here is the page body and the footer's measure. */
115
56
  main {
116
57
  max-width: var(--max);
117
58
  margin: 0 auto;
118
59
  padding: clamp(2rem, 6vh, 3.5rem) 1.5rem 4rem;
119
60
  }
120
-
121
- /* The footer's own rules are unscoped (footer.css, shared with the docs
122
- sites); this only puts it on the page's measure. */
123
- .foot {
124
- max-width: var(--max);
125
- margin: 0 auto;
126
- padding: 0 1.5rem;
127
- }
128
61
  </style>
129
62
 
130
63
  <!-- Prose styling for rendered markdown. Global rather than scoped: the
package/src/footer.css CHANGED
@@ -7,9 +7,9 @@
7
7
  * is why /blog and /updates shipped a dot-separated link row instead — the
8
8
  * shared footer would have rendered unstyled there.
9
9
  *
10
- * Imported by both `starlight.css` and `astro.css`; nothing needs to import it
10
+ * Imported by both `starlight.css` and `chrome.css`; nothing needs to import it
11
11
  * directly. Written against `--rx-*` tokens only, so it does not depend on the
12
- * short aliases astro.css defines.
12
+ * short aliases chrome.css defines.
13
13
  */
14
14
 
15
15
  @import './tokens.css';
package/src/index.ts CHANGED
@@ -15,6 +15,7 @@ export {
15
15
  getRepo,
16
16
  docsUrl,
17
17
  siteUrl,
18
+ canonicalUrl,
18
19
  type Project,
19
20
  type ProjectId,
20
21
  type RepoId,
package/src/sites.test.ts CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  getRepo,
19
19
  docsUrl,
20
20
  siteUrl,
21
+ canonicalUrl,
21
22
  projectFromBase,
22
23
  type ProjectId,
23
24
  } from './sites.ts'
@@ -111,6 +112,31 @@ describe('urls', () => {
111
112
  })
112
113
  })
113
114
 
115
+ describe('canonicalUrl', () => {
116
+ // The bug this exists to prevent: /blog answers 301 and only /blog/ answers
117
+ // 200, so a canonical without the slash points a crawler at a redirect.
118
+ it('always ends in a slash, whatever the caller passed', () => {
119
+ expect(canonicalUrl('/blog')).toBe(`${RXOVA_ORIGIN}/blog/`)
120
+ expect(canonicalUrl('/blog/')).toBe(`${RXOVA_ORIGIN}/blog/`)
121
+ expect(canonicalUrl('/updates/repos/journey')).toBe(`${RXOVA_ORIGIN}/updates/repos/journey/`)
122
+ })
123
+
124
+ it('roots a path that arrives without a leading slash', () => {
125
+ expect(canonicalUrl('blog')).toBe(`${RXOVA_ORIGIN}/blog/`)
126
+ })
127
+
128
+ it('defaults to the origin root', () => {
129
+ expect(canonicalUrl()).toBe(`${RXOVA_ORIGIN}/`)
130
+ })
131
+
132
+ // siteUrl builds nav hrefs, where following a redirect costs nothing. Keeping
133
+ // them different is the point; collapsing them would reintroduce the bug.
134
+ it('differs from siteUrl, which does not normalise', () => {
135
+ expect(siteUrl('/blog')).toBe(`${RXOVA_ORIGIN}/blog`)
136
+ expect(canonicalUrl('/blog')).not.toBe(siteUrl('/blog'))
137
+ })
138
+ })
139
+
114
140
  describe('projectFromBase', () => {
115
141
  it('recognises a mount with and without its trailing slash', () => {
116
142
  expect(projectFromBase('/packages/journey/')).toBe('journey')
package/src/sites.ts CHANGED
@@ -121,6 +121,23 @@ export function siteUrl(path = '/'): string {
121
121
  return `${RXOVA_ORIGIN}${path.startsWith('/') ? path : `/${path}`}`
122
122
  }
123
123
 
124
+ /**
125
+ * Absolute canonical URL for a *page* on the umbrella site.
126
+ *
127
+ * Trailing slash, always — which is the whole reason this is not `siteUrl`.
128
+ * rxova.org is published as a directory-style tree on GitHub Pages, so `/blog`
129
+ * answers 301 and only `/blog/` answers 200. A self-referencing canonical naming
130
+ * the redirecting form points a crawler at a URL that does not serve the page,
131
+ * which is the one thing a canonical must never do. `siteUrl` stays as it is:
132
+ * a nav link may follow a redirect, a canonical may not.
133
+ *
134
+ * Pages only. Assets keep their exact path and want `siteUrl`.
135
+ */
136
+ export function canonicalUrl(path = '/'): string {
137
+ const rooted = path.startsWith('/') ? path : `/${path}`
138
+ return `${RXOVA_ORIGIN}${rooted.endsWith('/') ? rooted : `${rooted}/`}`
139
+ }
140
+
124
141
  /**
125
142
  * Which project a page belongs to, inferred from Astro's `BASE_URL`.
126
143
  *
package/src/starlight.css CHANGED
@@ -78,6 +78,44 @@ header.header {
78
78
  background: var(--sl-color-bg-nav);
79
79
  }
80
80
 
81
+ /* In a schema-2 page bundle the umbrella header is rendered by rxova-website
82
+ outside Starlight's `.page`. Keep Starlight's own sticky documentation bar
83
+ below it instead of letting the two sticky headers occupy the same top edge. */
84
+ html[data-rxova-shell] {
85
+ --rx-shell-header-height: 3.5rem;
86
+ }
87
+
88
+ html[data-rxova-shell] .page > header.header {
89
+ inset-block-start: var(--rx-shell-header-height);
90
+ }
91
+
92
+ html[data-rxova-shell] .sidebar-pane {
93
+ inset-block-start: calc(var(--rx-shell-header-height) + var(--sl-nav-height));
94
+ }
95
+
96
+ html[data-rxova-shell] mobile-starlight-toc nav {
97
+ top: calc(var(--rx-shell-header-height) + var(--sl-nav-height) - 1px);
98
+ }
99
+
100
+ html[data-rxova-shell] starlight-menu-button button {
101
+ top: calc(
102
+ var(--rx-shell-header-height) + (var(--sl-nav-height) - var(--sl-menu-button-size)) / 2
103
+ );
104
+ }
105
+
106
+ @media (min-width: 72rem) {
107
+ html[data-rxova-shell] .right-sidebar {
108
+ top: var(--rx-shell-header-height);
109
+ height: calc(100vh - var(--rx-shell-header-height));
110
+ }
111
+ }
112
+
113
+ @media (max-width: 34rem) {
114
+ html[data-rxova-shell] {
115
+ --rx-shell-header-height: 3.25rem;
116
+ }
117
+ }
118
+
81
119
  /* Site title: the rxova mark sits beside the project wordmark. */
82
120
  .rx-site-title {
83
121
  display: flex;
package/src/starlight.ts CHANGED
@@ -27,6 +27,12 @@ export interface SharedStarlightOptions {
27
27
  components?: Record<string, string>
28
28
  /** Path under the repo root that holds the docs site, for the edit link. */
29
29
  editLinkBase?: string
30
+ /**
31
+ * Build body-only documentation for rxova-website to place in its global
32
+ * shell. Starlight's search, sidebar and page navigation remain page UI; only
33
+ * the umbrella footer is omitted.
34
+ */
35
+ pageComponent?: boolean
30
36
  }
31
37
 
32
38
  export function sharedStarlightConfig({
@@ -35,6 +41,7 @@ export function sharedStarlightConfig({
35
41
  customCss = [],
36
42
  components = {},
37
43
  editLinkBase = 'apps/docs',
44
+ pageComponent = false,
38
45
  }: SharedStarlightOptions) {
39
46
  const self = getProject(project)
40
47
 
@@ -72,7 +79,7 @@ export function sharedStarlightConfig({
72
79
  SocialIcons: '@rxova/brand/components/SocialIcons.astro',
73
80
  // Starlight's default footer (pagination, edit link, last updated) plus
74
81
  // the shared four-column site footer beneath it.
75
- Footer: '@rxova/brand/components/Footer.astro',
82
+ ...(!pageComponent ? { Footer: '@rxova/brand/components/Footer.astro' } : {}),
76
83
  // Starlight's own picker, plus a resync when a page is restored from the
77
84
  // back/forward cache — without it, changing the theme on one rxova.org
78
85
  // surface and pressing Back leaves the restored page on the old theme.