@rxova/brand 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jonatan Kruszewski
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,133 @@
1
+ <h1 align="center">@rxova/brand</h1>
2
+
3
+ <p align="center">
4
+ Design tokens, Starlight theme and shared site chrome for every
5
+ <a href="https://rxova.org">rxova.org</a> surface.
6
+ </p>
7
+
8
+ ---
9
+
10
+ ## Why this exists
11
+
12
+ `rxova.org` serves four surfaces from one origin — an Astro landing at `/`, plus
13
+ [journey](https://github.com/rxova/journey),
14
+ [react-inputs](https://github.com/rxova/react-inputs) and
15
+ [use-everywhere](https://github.com/rxova/use-everywhere) docs, each built in its
16
+ own repo and mounted as a static tree under `/packages/<name>/`.
17
+
18
+ They were built independently and ended up looking like four unrelated products:
19
+ three different accent colours, three different footers, no way to navigate from
20
+ one project's docs to another's. This package is the single source of truth that
21
+ makes them one site — and it lives in its own repo because it is consumed by four
22
+ and owned by none.
23
+
24
+ ## Install
25
+
26
+ ```sh
27
+ pnpm add @rxova/brand
28
+ ```
29
+
30
+ ## Use
31
+
32
+ ### In a Starlight docs site
33
+
34
+ ```js
35
+ // astro.config.mjs
36
+ import { defineConfig } from 'astro/config'
37
+ import starlight from '@astrojs/starlight'
38
+ import { sharedStarlightConfig } from '@rxova/brand'
39
+
40
+ export default defineConfig({
41
+ site: process.env.DOCS_URL ?? 'https://rxova.org',
42
+ // The aggregator mounts this build under its own path.
43
+ base: process.env.DOCS_BASE_URL ?? '/',
44
+ integrations: [
45
+ starlight(
46
+ sharedStarlightConfig({
47
+ project: 'use-everywhere',
48
+ // Note the nesting: Starlight 0.39 removed `label` + `autogenerate`
49
+ // as siblings, so an autogenerated group goes inside `items`.
50
+ sidebar: [{ label: 'Learn', items: [{ autogenerate: { directory: 'learn' } }] }],
51
+ }),
52
+ ),
53
+ ],
54
+ })
55
+ ```
56
+
57
+ That gets you the tokens, the typefaces, the rxova mark linking back to the
58
+ umbrella site, the cross-project switcher, the shared four-column footer, and
59
+ Pagefind search.
60
+
61
+ ### In a plain Astro site
62
+
63
+ ```astro
64
+ ---
65
+ import '@rxova/brand/fonts.css'
66
+ import '@rxova/brand/astro.css'
67
+ import SiteFooter from '@rxova/brand/components/SiteFooter.astro'
68
+ import { PROJECTS } from '@rxova/brand'
69
+ ---
70
+ ```
71
+
72
+ `astro.css` pulls in `tokens.css` and adds the reset and base element styles
73
+ that Starlight gives docs sites for free.
74
+
75
+ ## What's in it
76
+
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 |
86
+
87
+ ## The palette
88
+
89
+ A warm monochrome — warm near-black ink on warm paper — with the logo's
90
+ blue → violet → magenta gradient as the **only** chroma in the system. The
91
+ restraint is the identity; resist adding a second accent hue.
92
+
93
+ The gradient earns exactly two placements: the hairline under the site header,
94
+ and the current-project dot in the switcher.
95
+
96
+ Neutrals flip between light and dark, so a downstream mapping references a token
97
+ once and gets both modes for free.
98
+
99
+ ## Development
100
+
101
+ ```sh
102
+ pnpm install
103
+ pnpm run verify # format, lint, tsc, astro check, social cards, package contract
104
+ pnpm run og # re-render the social cards after a palette or tagline change
105
+ ```
106
+
107
+ There is no build step: Astro resolves `.ts` and `.astro` straight from
108
+ `node_modules` (Starlight ships the same way). That means nothing would catch a
109
+ file missing from `files` or a stale `exports` path, which is what
110
+ `check-exports.mjs` is for — it packs the real tarball and asserts every declared
111
+ subpath resolves.
112
+
113
+ The social cards are generated from `tokens.css` and `sites.ts`, so they cannot
114
+ drift from the brand — but they are committed, so a palette or tagline change
115
+ silently invalidates them. `check:og` compares a hash of those _inputs_ against
116
+ `scripts/og-manifest.json` rather than re-rendering and diffing the PNGs: resvg
117
+ ships per-platform native builds, and an output diff would fail CI for cards that
118
+ are perfectly correct.
119
+
120
+ ### Trying a change in a consumer
121
+
122
+ The package has no `workspace:*` shortcut, so iterate through snapshot releases:
123
+
124
+ 1. Add a changeset (`pnpm exec changeset`).
125
+ 2. Run the **Snapshot** workflow — publishes `0.x.y-next.N` to the `next` tag.
126
+ 3. In the consumer: `pnpm add @rxova/brand@next`.
127
+
128
+ Slower per iteration than linking, but CI resolves exactly what you resolved, so
129
+ a broken export map fails on your machine rather than three repos later.
130
+
131
+ ## License
132
+
133
+ MIT © Jonatan Kruszewski
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json ADDED
@@ -0,0 +1,97 @@
1
+ {
2
+ "name": "@rxova/brand",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Design tokens, Starlight theme and shared site chrome for rxova.org",
6
+ "keywords": [
7
+ "rxova",
8
+ "design-tokens",
9
+ "astro",
10
+ "starlight",
11
+ "theme"
12
+ ],
13
+ "license": "MIT",
14
+ "author": "Jonatan Kruszewski <jonakrusze@gmail.com>",
15
+ "homepage": "https://rxova.org",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/rxova/brand.git"
19
+ },
20
+ "bugs": "https://github.com/rxova/brand/issues",
21
+ "packageManager": "pnpm@11.9.0",
22
+ "engines": {
23
+ "node": ">=20.19.0"
24
+ },
25
+ "sideEffects": [
26
+ "*.css"
27
+ ],
28
+ "files": [
29
+ "src",
30
+ "assets"
31
+ ],
32
+ "exports": {
33
+ ".": "./src/index.ts",
34
+ "./tokens.css": "./src/tokens.css",
35
+ "./astro.css": "./src/astro.css",
36
+ "./starlight.css": "./src/starlight.css",
37
+ "./fonts.css": "./src/fonts.css",
38
+ "./components/*": "./src/components/*",
39
+ "./assets/*": "./assets/*",
40
+ "./package.json": "./package.json"
41
+ },
42
+ "peerDependencies": {
43
+ "@astrojs/starlight": ">=0.36.0",
44
+ "astro": ">=5.6.0"
45
+ },
46
+ "peerDependenciesMeta": {
47
+ "@astrojs/starlight": {
48
+ "optional": true
49
+ }
50
+ },
51
+ "scripts": {
52
+ "lint": "eslint .",
53
+ "lint:fix": "eslint . --fix",
54
+ "format": "prettier --write .",
55
+ "format:check": "prettier --check .",
56
+ "typecheck": "tsc --noEmit",
57
+ "check:astro": "astro check",
58
+ "check:exports": "node scripts/check-exports.mjs",
59
+ "og": "node scripts/generate-og.mjs",
60
+ "check:og": "node scripts/generate-og.mjs --check",
61
+ "verify": "pnpm run format:check && pnpm run lint && pnpm run typecheck && pnpm run check:astro && pnpm run check:og && pnpm run check:exports",
62
+ "prepare": "husky"
63
+ },
64
+ "dependencies": {
65
+ "@fontsource/ibm-plex-mono": "^5.2.5",
66
+ "@fontsource/space-grotesk": "^5.2.5"
67
+ },
68
+ "devDependencies": {
69
+ "@astrojs/check": "^0.9.9",
70
+ "@astrojs/starlight": "^0.41.4",
71
+ "@changesets/changelog-github": "^0.7.0",
72
+ "@changesets/cli": "^2.29.7",
73
+ "@commitlint/cli": "^21.2.1",
74
+ "@commitlint/config-conventional": "^21.2.0",
75
+ "@eslint/js": "^10.0.1",
76
+ "@resvg/resvg-js": "^2.6.2",
77
+ "@types/node": "^26.1.1",
78
+ "astro": "^7.1.3",
79
+ "eslint": "^10.7.0",
80
+ "eslint-plugin-astro": "^1.3.0",
81
+ "globals": "^17.6.0",
82
+ "husky": "^9.1.7",
83
+ "lint-staged": "^17.2.0",
84
+ "prettier": "^3.9.6",
85
+ "prettier-plugin-astro": "^0.14.0",
86
+ "satori": "^0.29.0",
87
+ "typescript": "6.0.3",
88
+ "typescript-eslint": "^8.65.0"
89
+ },
90
+ "lint-staged": {
91
+ "*.{ts,tsx,astro,mjs,js}": [
92
+ "eslint --fix",
93
+ "prettier --write"
94
+ ],
95
+ "*.{json,jsonc,css,md,mdx,yaml,yml}": "prettier --write"
96
+ }
97
+ }
package/src/astro.css ADDED
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Entry point for plain Astro surfaces — today, the rxova.org landing.
3
+ *
4
+ * 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 already provides: a reset and base element styling.
7
+ *
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.
13
+ */
14
+
15
+ @import './tokens.css';
16
+
17
+ :root {
18
+ --bg: var(--rx-bg);
19
+ --fg: var(--rx-fg);
20
+ --muted: var(--rx-muted);
21
+ --faint: var(--rx-faint);
22
+ --rule: var(--rx-rule);
23
+ --card: var(--rx-card);
24
+ --tag-bg: var(--rx-tag-bg);
25
+ --glow-tint: var(--rx-glow-tint);
26
+ --font: var(--rx-font-sans);
27
+ --mono: var(--rx-font-mono);
28
+ --max: var(--rx-max);
29
+
30
+ /* The landing's `--accent` is the foreground, not the brand violet: its
31
+ design is deliberately monochrome and uses the gradient only as an accent
32
+ rule. Mapping this to --rx-primary would tint every focus ring on the
33
+ site. */
34
+ --accent: var(--rx-fg);
35
+ }
36
+
37
+ /* --- Reset ---------------------------------------------------------------- */
38
+
39
+ *,
40
+ *::before,
41
+ *::after {
42
+ box-sizing: border-box;
43
+ margin: 0;
44
+ padding: 0;
45
+ }
46
+
47
+ html {
48
+ -webkit-text-size-adjust: 100%;
49
+ scroll-behavior: smooth;
50
+ }
51
+
52
+ @media (prefers-reduced-motion: reduce) {
53
+ html {
54
+ scroll-behavior: auto;
55
+ }
56
+ }
57
+
58
+ body {
59
+ position: relative;
60
+ background: var(--rx-bg);
61
+ color: var(--rx-fg);
62
+ font-family: var(--rx-font-sans);
63
+ font-size: 1.0625rem;
64
+ line-height: 1.65;
65
+ -webkit-font-smoothing: antialiased;
66
+ text-rendering: optimizeLegibility;
67
+ transition:
68
+ background-color 0.2s ease,
69
+ color 0.2s ease;
70
+ }
71
+
72
+ a {
73
+ color: inherit;
74
+ }
75
+
76
+ a:focus-visible {
77
+ outline: 2px solid var(--rx-fg);
78
+ outline-offset: 3px;
79
+ border-radius: 2px;
80
+ }
81
+
82
+ img {
83
+ max-width: 100%;
84
+ height: auto;
85
+ }
@@ -0,0 +1,18 @@
1
+ ---
2
+ /**
3
+ * Starlight `Footer` override.
4
+ *
5
+ * Keeps everything Starlight puts at the bottom of a doc page — pagination,
6
+ * "edit this page", last-updated — and adds the shared rxova site footer
7
+ * beneath it. Replacing rather than wrapping would have cost the pagination,
8
+ * which is load-bearing navigation on a docs site.
9
+ */
10
+ import Default from '@astrojs/starlight/components/Footer.astro'
11
+ import SiteFooter from './SiteFooter.astro'
12
+ import { projectFromBase } from '../sites.ts'
13
+
14
+ const project = projectFromBase(import.meta.env.BASE_URL)
15
+ ---
16
+
17
+ <Default><slot /></Default>
18
+ <SiteFooter project={project} />
@@ -0,0 +1,102 @@
1
+ ---
2
+ /**
3
+ * Cross-project navigation.
4
+ *
5
+ * rxova.org serves four surfaces from one origin, and before this existed there
6
+ * was no way to get from one project's docs to another's without going back to
7
+ * the landing. Every href is absolute — a relative one would resolve under the
8
+ * current site's base path.
9
+ *
10
+ * Rendered as a `<details>` so it works with zero JavaScript; the current
11
+ * project gets the gradient dot (see `.rx-switcher__current` in starlight.css).
12
+ */
13
+ import { PROJECTS, RXOVA_ORIGIN, type ProjectId } from '../sites.ts'
14
+
15
+ interface Props {
16
+ /** Which project this site is, so it can be marked as current. */
17
+ current?: ProjectId
18
+ }
19
+
20
+ const { current } = Astro.props
21
+ ---
22
+
23
+ <details class="rx-switcher">
24
+ <summary class="rx-switcher__trigger" aria-label="Switch project">
25
+ Projects
26
+ <svg aria-hidden="true" viewBox="0 0 16 16" width="12" height="12">
27
+ <path d="M4 6l4 4 4-4" fill="none" stroke="currentColor" stroke-width="1.75"></path>
28
+ </svg>
29
+ </summary>
30
+ <ul class="rx-switcher__menu">
31
+ <li><a href={RXOVA_ORIGIN}>rxova.org</a></li>
32
+ {
33
+ PROJECTS.map((project) => (
34
+ <li>
35
+ <a
36
+ href={`${RXOVA_ORIGIN}${project.mount}`}
37
+ class={project.id === current ? 'rx-switcher__current' : undefined}
38
+ aria-current={project.id === current ? 'page' : undefined}
39
+ >
40
+ {project.label}
41
+ </a>
42
+ </li>
43
+ ))
44
+ }
45
+ </ul>
46
+ </details>
47
+
48
+ <style>
49
+ .rx-switcher__trigger {
50
+ display: flex;
51
+ align-items: center;
52
+ gap: 0.3rem;
53
+ padding: 0.25rem 0.5rem;
54
+ border-radius: var(--rx-radius-sm);
55
+ color: var(--sl-color-gray-2);
56
+ font-size: var(--sl-text-sm);
57
+ cursor: pointer;
58
+ list-style: none;
59
+ }
60
+
61
+ .rx-switcher__trigger::-webkit-details-marker {
62
+ display: none;
63
+ }
64
+
65
+ .rx-switcher__trigger:hover {
66
+ color: var(--sl-color-white);
67
+ background: var(--sl-color-gray-6);
68
+ }
69
+
70
+ .rx-switcher[open] > .rx-switcher__trigger svg {
71
+ transform: rotate(180deg);
72
+ }
73
+
74
+ .rx-switcher__menu {
75
+ position: absolute;
76
+ z-index: var(--sl-z-index-navbar, 20);
77
+ inset-inline-end: 0;
78
+ min-width: 12rem;
79
+ margin: 0.4rem 0 0;
80
+ padding: 0.35rem;
81
+ border: 1px solid var(--sl-color-hairline);
82
+ border-radius: var(--rx-radius);
83
+ background: var(--sl-color-bg-nav);
84
+ box-shadow: var(--sl-shadow-md);
85
+ list-style: none;
86
+ }
87
+
88
+ .rx-switcher__menu a {
89
+ display: flex;
90
+ align-items: center;
91
+ padding: 0.4rem 0.6rem;
92
+ border-radius: var(--rx-radius-sm);
93
+ color: var(--sl-color-gray-2);
94
+ font-size: var(--sl-text-sm);
95
+ text-decoration: none;
96
+ }
97
+
98
+ .rx-switcher__menu a:hover {
99
+ color: var(--sl-color-white);
100
+ background: var(--sl-color-gray-6);
101
+ }
102
+ </style>
@@ -0,0 +1,96 @@
1
+ ---
2
+ /**
3
+ * The four-column rxova footer, shared by all four surfaces.
4
+ *
5
+ * Before this, the landing had a single dot-separated link row and the three
6
+ * docs sites had a bare copyright line (journey went further and hid its footer
7
+ * links outright). The footer is the second-best place, after the switcher, to
8
+ * make one origin feel like one site — so it carries the full project list on
9
+ * every page.
10
+ *
11
+ * All cross-project hrefs are absolute; see sites.ts for why.
12
+ */
13
+ import { PROJECTS, RXOVA_ORIGIN, getProject, siteUrl, type ProjectId } from '../sites.ts'
14
+
15
+ interface Props {
16
+ /** Current project, if this is a docs site. Omitted on the landing. */
17
+ project?: ProjectId
18
+ /** Optional per-site "Docs" column. */
19
+ docs?: { label: string; href: string }[]
20
+ }
21
+
22
+ const { project, docs } = Astro.props
23
+ const self = project ? getProject(project) : undefined
24
+ const year = new Date().getFullYear()
25
+ ---
26
+
27
+ <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) => (
35
+ <li>
36
+ <a href={link.href}>{link.label}</a>
37
+ </li>
38
+ ))}
39
+ </ul>
40
+ </div>
41
+ )
42
+ }
43
+
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>
57
+
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
+ }
82
+
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>
92
+ </div>
93
+ </div>
94
+
95
+ <p class="rx-footer__legal">© {year} rxova · MIT licensed</p>
96
+ </footer>
@@ -0,0 +1,44 @@
1
+ ---
2
+ /**
3
+ * Starlight `SiteTitle` override.
4
+ *
5
+ * The mark links to rxova.org, not to the local docs home. That is deliberate:
6
+ * on an aggregated site the mark means "the umbrella", and the wordmark beside
7
+ * it says which project you are in — and links to that project's docs root.
8
+ *
9
+ * The mark's href is absolute because the four surfaces sit at four base paths.
10
+ * The wordmark uses Starlight's own `siteTitleHref`, which already accounts for
11
+ * the base and the locale.
12
+ *
13
+ * The image is imported from this package rather than read from each site's
14
+ * `public/`, so adopting the shared chrome does not also mean copying assets
15
+ * into three repos and keeping them in sync.
16
+ */
17
+ import mark from '../../assets/rxova-logo-256.png'
18
+ import { RXOVA_ORIGIN } from '../sites.ts'
19
+
20
+ // Narrowed structurally rather than by declaring an `App.Locals` augmentation
21
+ // here: a consumer site already gets Starlight's own augmentation, and two
22
+ // declarations of the same property would collide at their build time.
23
+ const { siteTitle, siteTitleHref } = (
24
+ Astro.locals as unknown as {
25
+ starlightRoute: { siteTitle: string; siteTitleHref: string }
26
+ }
27
+ ).starlightRoute
28
+ ---
29
+
30
+ <div class="rx-site-title">
31
+ <a href={RXOVA_ORIGIN} class="rx-site-title__mark" aria-label="rxova home">
32
+ <img src={mark.src} alt="" width="28" height="28" />
33
+ </a>
34
+ <span class="rx-site-title__sep" aria-hidden="true"></span>
35
+ <a href={siteTitleHref} class="rx-site-title__project" translate="no">{siteTitle}</a>
36
+ </div>
37
+
38
+ <style>
39
+ /* Visual styling lives in @rxova/brand/starlight.css so the Astro landing can
40
+ reuse the same classnames without importing this component. */
41
+ .rx-site-title__mark {
42
+ display: inline-flex;
43
+ }
44
+ </style>
@@ -0,0 +1,19 @@
1
+ ---
2
+ /**
3
+ * Starlight `SocialIcons` override: the stock icons, plus the project switcher.
4
+ *
5
+ * Starlight has no dedicated slot in the header's right-hand cluster, and this
6
+ * is the documented place to add one — it sits immediately before search and
7
+ * the theme toggle on every page.
8
+ */
9
+ import Default from '@astrojs/starlight/components/SocialIcons.astro'
10
+ import ProjectSwitcher from './ProjectSwitcher.astro'
11
+ import { projectFromBase } from '../sites.ts'
12
+
13
+ // Overrides receive no props, so the current project is inferred from the base
14
+ // path the aggregator mounts this site at.
15
+ const current = projectFromBase(import.meta.env.BASE_URL)
16
+ ---
17
+
18
+ <ProjectSwitcher current={current} />
19
+ <Default><slot /></Default>
package/src/env.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ /// <reference types="astro/client" />
2
+
3
+ // Deliberately NOT referencing `@astrojs/starlight/locals` for the
4
+ // `Astro.locals.starlightRoute` augmentation: that declaration pulls in
5
+ // Starlight's own `.ts` sources, which import `virtual:starlight/*` modules
6
+ // that only exist inside an Astro build — so plain `tsc` fails on them.
7
+ // The one component that reads route data narrows it structurally instead.
package/src/fonts.css ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Self-hosted brand typefaces.
3
+ *
4
+ * Space Grotesk + IBM Plex Mono came from the journey docs, which was the only
5
+ * rxova surface with a real typographic identity. They are bundled here rather
6
+ * than pulled from Google Fonts at runtime: journey's original
7
+ * `@import url("https://fonts.googleapis.com/...")` sat inside a stylesheet, so
8
+ * it was a render-blocking third-party request on the critical path (bad LCP),
9
+ * and it leaked visitor IPs to a third party on an EU-facing site.
10
+ *
11
+ * Weights are deliberately narrow — the ones the design system actually uses.
12
+ * Adding a weight here costs every page on rxova.org, so add only on demand.
13
+ */
14
+
15
+ /* Body, headings and UI. */
16
+ @import '@fontsource/space-grotesk/400.css';
17
+ @import '@fontsource/space-grotesk/500.css';
18
+ @import '@fontsource/space-grotesk/600.css';
19
+ @import '@fontsource/space-grotesk/700.css';
20
+
21
+ /* Code, package names and install snippets. */
22
+ @import '@fontsource/ibm-plex-mono/400.css';
23
+ @import '@fontsource/ibm-plex-mono/500.css';
package/src/index.ts ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @rxova/brand — design tokens, Starlight theme and shared chrome for rxova.org.
3
+ *
4
+ * Stylesheets and components are reached through their own subpath exports
5
+ * (`@rxova/brand/tokens.css`, `@rxova/brand/components/SiteFooter.astro`) so this
6
+ * entry point stays importable from `astro.config.mjs` under plain Node.
7
+ */
8
+
9
+ export {
10
+ RXOVA_ORIGIN,
11
+ PROJECTS,
12
+ getProject,
13
+ docsUrl,
14
+ siteUrl,
15
+ type Project,
16
+ type ProjectId,
17
+ } from './sites.ts'
18
+
19
+ export { sharedStarlightConfig, type SharedStarlightOptions } from './starlight.ts'
package/src/sites.ts ADDED
@@ -0,0 +1,96 @@
1
+ /**
2
+ * The rxova.org site map.
3
+ *
4
+ * rxova.org is an aggregator: an Astro landing at `/`, plus each project's docs
5
+ * built in its own repo and mounted as a static tree under `/packages/<name>/`.
6
+ * That means four surfaces live on one origin at four different base paths, so
7
+ * **every cross-project link must be absolute**. A relative href would resolve
8
+ * against the local base and produce `/packages/journey/packages/react-inputs/`.
9
+ *
10
+ * This module is imported from `astro.config.mjs` under Node, so it must never
11
+ * import CSS or a component.
12
+ */
13
+
14
+ /** Canonical origin. Override for a staging deploy (e.g. https://web.rxova.org). */
15
+ export const RXOVA_ORIGIN = process.env.RXOVA_ORIGIN ?? 'https://rxova.org'
16
+
17
+ export type ProjectId = 'journey' | 'react-inputs' | 'use-everywhere'
18
+
19
+ export interface Project {
20
+ id: ProjectId
21
+ /** Display name, used in the header, the switcher and the landing cards. */
22
+ label: string
23
+ /** Path the aggregator mounts this project's docs at. Leading and trailing slash. */
24
+ mount: `/${string}/`
25
+ tagline: string
26
+ repo: string
27
+ /** The package a newcomer should install first. */
28
+ npm: string
29
+ /** npm package names, most prominent first. */
30
+ packages: string[]
31
+ }
32
+
33
+ export const PROJECTS: readonly Project[] = [
34
+ {
35
+ id: 'journey',
36
+ label: 'Journey',
37
+ mount: '/packages/journey/',
38
+ tagline: 'Declarative journey graphs for non-linear UI flows.',
39
+ repo: 'https://github.com/rxova/journey',
40
+ npm: 'https://www.npmjs.com/package/@rxova/journey-core',
41
+ packages: ['@rxova/journey-core', '@rxova/journey-react', '@rxova/journey-devtools-bridge'],
42
+ },
43
+ {
44
+ id: 'react-inputs',
45
+ label: 'react-inputs',
46
+ mount: '/packages/react-inputs/',
47
+ tagline: 'The tricky React inputs, done right.',
48
+ repo: 'https://github.com/rxova/react-inputs',
49
+ npm: 'https://www.npmjs.com/package/@rxova/react-inputs',
50
+ packages: [
51
+ '@rxova/react-inputs',
52
+ '@rxova/react-intl-currency-input',
53
+ '@rxova/react-otp-input',
54
+ '@rxova/react-rating-input',
55
+ ],
56
+ },
57
+ {
58
+ id: 'use-everywhere',
59
+ label: 'use-everywhere',
60
+ mount: '/packages/use-everywhere/',
61
+ tagline: 'State and messages that exist in every tab, window, and worker.',
62
+ repo: 'https://github.com/rxova/use-everywhere',
63
+ npm: 'https://www.npmjs.com/package/use-everywhere',
64
+ packages: ['use-everywhere', '@use-everywhere/core'],
65
+ },
66
+ ] as const
67
+
68
+ export function getProject(id: ProjectId): Project {
69
+ const project = PROJECTS.find((p) => p.id === id)
70
+ if (!project) throw new Error(`[@rxova/brand] unknown project id: ${id}`)
71
+ return project
72
+ }
73
+
74
+ /** Absolute URL to a project's docs root. */
75
+ export function docsUrl(id: ProjectId): string {
76
+ return `${RXOVA_ORIGIN}${getProject(id).mount}`
77
+ }
78
+
79
+ /** Absolute URL to a path on the umbrella site, e.g. `/privacy`. */
80
+ export function siteUrl(path = '/'): string {
81
+ return `${RXOVA_ORIGIN}${path.startsWith('/') ? path : `/${path}`}`
82
+ }
83
+
84
+ /**
85
+ * Which project a page belongs to, inferred from Astro's `BASE_URL`.
86
+ *
87
+ * Component overrides can't be given props, so the shared chrome works this out
88
+ * for itself rather than making every repo declare it twice (once in the
89
+ * Starlight config, once in the override). Returns `undefined` on a standalone
90
+ * build where the base is `/` and there is nothing to infer from — the current
91
+ * marker is simply omitted, which is correct off the aggregator.
92
+ */
93
+ export function projectFromBase(base: string): ProjectId | undefined {
94
+ const normalised = base.endsWith('/') ? base : `${base}/`
95
+ return PROJECTS.find((p) => p.mount === normalised)?.id
96
+ }
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Maps rxova tokens onto Starlight's theme variables.
3
+ *
4
+ * Load this from a Starlight site's `customCss`, which Astro appends after
5
+ * Starlight's own stylesheets — so these declarations win without !important.
6
+ *
7
+ * Every --rx-* neutral already flips between light and dark (see tokens.css),
8
+ * which is why one mapping block covers both modes. Starlight's own defaults
9
+ * are dark-in-:root / light-in-[data-theme='light']; we don't need that split
10
+ * because its inline head script always stamps an explicit data-theme before
11
+ * first paint.
12
+ *
13
+ * Starlight's gray ramp runs 1 (highest contrast) -> 7 (lowest), which is the
14
+ * same direction as our text -> surface ladder, so the mapping is monotonic in
15
+ * both modes.
16
+ */
17
+
18
+ @import './tokens.css';
19
+
20
+ :root {
21
+ /* Accent. */
22
+ --sl-color-accent: var(--rx-primary);
23
+ --sl-color-accent-low: color-mix(in srgb, var(--rx-primary) 20%, var(--rx-bg));
24
+ --sl-color-accent-high: var(--rx-primary-lightest);
25
+ --sl-color-text-accent: var(--rx-primary);
26
+
27
+ /* Neutral ladder. "white" and "black" are Starlight's names for the two
28
+ contrast extremes, not literal colours — they invert with the theme. */
29
+ --sl-color-white: var(--rx-fg);
30
+ --sl-color-gray-1: var(--rx-fg);
31
+ --sl-color-gray-2: var(--rx-muted);
32
+ --sl-color-gray-3: var(--rx-faint);
33
+ --sl-color-gray-4: var(--rx-rule-strong);
34
+ --sl-color-gray-5: var(--rx-rule);
35
+ --sl-color-gray-6: var(--rx-tag-bg);
36
+ --sl-color-gray-7: var(--rx-card);
37
+ --sl-color-black: var(--rx-bg);
38
+
39
+ /* Surfaces. Nav and sidebar sit flush with the page — the hairline does the
40
+ separating, not a fill change. */
41
+ --sl-color-bg: var(--rx-bg);
42
+ --sl-color-bg-nav: var(--rx-bg);
43
+ --sl-color-bg-sidebar: var(--rx-bg);
44
+ --sl-color-bg-inline-code: var(--rx-tag-bg);
45
+ --sl-color-bg-badge: var(--rx-tag-bg);
46
+
47
+ --sl-color-hairline: var(--rx-rule);
48
+ --sl-color-hairline-light: var(--rx-rule);
49
+ --sl-color-hairline-shade: var(--rx-rule-strong);
50
+ --sl-color-backdrop-overlay: color-mix(in srgb, var(--rx-bg) 80%, transparent);
51
+
52
+ /* Type. */
53
+ --sl-font: var(--rx-font-sans);
54
+ --sl-font-mono: var(--rx-font-mono);
55
+
56
+ --sl-shadow-sm: 0 1px 2px rgb(0 0 0 / 0.06);
57
+ --sl-shadow-md: 0 4px 12px -4px rgb(0 0 0 / 0.12);
58
+ --sl-shadow-lg: var(--rx-shadow-soft);
59
+ }
60
+
61
+ /* Starlight ships a slightly cooler focus ring; align it with the accent. */
62
+ :root {
63
+ --sl-color-focus: var(--rx-primary);
64
+ }
65
+
66
+ /* --- Shared chrome ------------------------------------------------------- */
67
+
68
+ /* The gradient is the brand's only chroma, so it earns exactly two placements:
69
+ the hairline under the site header, and the current-project dot in the
70
+ project switcher. Adding a third dilutes both. */
71
+ .site-title::after {
72
+ content: none;
73
+ }
74
+
75
+ header.header {
76
+ border-bottom: 1px solid var(--sl-color-hairline);
77
+ background: var(--sl-color-bg-nav);
78
+ }
79
+
80
+ /* Site title: the rxova mark sits beside the project wordmark. */
81
+ .rx-site-title {
82
+ display: flex;
83
+ align-items: center;
84
+ gap: 0.625rem;
85
+ color: var(--rx-fg);
86
+ font-weight: 600;
87
+ letter-spacing: -0.01em;
88
+ text-decoration: none;
89
+ }
90
+
91
+ .rx-site-title a {
92
+ color: inherit;
93
+ text-decoration: none;
94
+ }
95
+
96
+ .rx-site-title a:hover {
97
+ color: var(--rx-fg);
98
+ }
99
+
100
+ .rx-site-title img {
101
+ width: 28px;
102
+ height: 28px;
103
+ border-radius: var(--rx-radius-sm);
104
+ }
105
+
106
+ .rx-site-title__sep {
107
+ width: 1px;
108
+ height: 1.1rem;
109
+ background: var(--rx-rule-strong);
110
+ }
111
+
112
+ .rx-site-title__project {
113
+ color: var(--rx-muted);
114
+ font-weight: 500;
115
+ }
116
+
117
+ /* Project switcher. */
118
+ .rx-switcher {
119
+ position: relative;
120
+ }
121
+
122
+ .rx-switcher__current::before {
123
+ content: '';
124
+ display: inline-block;
125
+ width: 0.5rem;
126
+ height: 0.5rem;
127
+ margin-inline-end: 0.5rem;
128
+ border-radius: var(--rx-radius-pill);
129
+ background: var(--rx-gradient);
130
+ }
131
+
132
+ /* --- Footer -------------------------------------------------------------- */
133
+
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
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Shared Starlight configuration for the rxova docs sites.
3
+ *
4
+ * Each project's `astro.config.mjs` supplies only what is genuinely its own —
5
+ * its sidebar, its extra plugins — and spreads the rest from here, so the three
6
+ * doc sites cannot drift apart in the ways that made them feel like three
7
+ * unrelated products.
8
+ *
9
+ * Imported from `astro.config.mjs` under Node: no CSS, no component imports.
10
+ *
11
+ * The return type is structural rather than Starlight's own `StarlightUserConfig`.
12
+ * Spreading into `starlight({ ... })` still type-checks the merged object at the
13
+ * call site, and this way a Starlight minor release cannot break every consumer
14
+ * at once over a type-only change.
15
+ */
16
+
17
+ import { RXOVA_ORIGIN, getProject, type ProjectId } from './sites.ts'
18
+
19
+ export interface SharedStarlightOptions {
20
+ /** Which project's docs this site is. */
21
+ project: ProjectId
22
+ /** Starlight sidebar config — the one thing every site defines itself. */
23
+ sidebar: unknown[]
24
+ /** Extra stylesheets, appended after the brand ones so they win. */
25
+ customCss?: string[]
26
+ /** Extra Starlight component overrides, merged over the shared ones. */
27
+ components?: Record<string, string>
28
+ /** Path under the repo root that holds the docs site, for the edit link. */
29
+ editLinkBase?: string
30
+ }
31
+
32
+ export function sharedStarlightConfig({
33
+ project,
34
+ sidebar,
35
+ customCss = [],
36
+ components = {},
37
+ editLinkBase = 'apps/docs',
38
+ }: SharedStarlightOptions) {
39
+ const self = getProject(project)
40
+
41
+ return {
42
+ title: self.label,
43
+ description: self.tagline,
44
+ // One origin, one tab icon. Each site must have this file in `public/` —
45
+ // Starlight resolves `favicon` against the site's own static directory, so
46
+ // it cannot come from this package.
47
+ favicon: '/favicon.svg',
48
+
49
+ // No `logo` here on purpose: the SiteTitle override renders the mark from
50
+ // this package's own assets, so adopting the shared chrome does not also
51
+ // mean copying an image into three repos and keeping it in sync.
52
+
53
+ social: [
54
+ { icon: 'github' as const, label: 'GitHub', href: self.repo },
55
+ { icon: 'npm' as const, label: 'npm', href: self.npm },
56
+ ],
57
+
58
+ editLink: {
59
+ baseUrl: `${self.repo}/edit/main/${editLinkBase}/`,
60
+ },
61
+
62
+ // Order matters: fonts, then tokens+mapping, then per-site overrides.
63
+ customCss: ['@rxova/brand/fonts.css', '@rxova/brand/starlight.css', ...customCss],
64
+
65
+ components: {
66
+ // The rxova mark + project wordmark, with the mark linking back to the
67
+ // umbrella site. This is the only "you are inside rxova.org" affordance
68
+ // on a docs page, so it ships shared rather than per-repo.
69
+ SiteTitle: '@rxova/brand/components/SiteTitle.astro',
70
+ // Appends the cross-project switcher to the social icons. Without it the
71
+ // three docs sites are three islands under one domain.
72
+ SocialIcons: '@rxova/brand/components/SocialIcons.astro',
73
+ // Starlight's default footer (pagination, edit link, last updated) plus
74
+ // the shared four-column site footer beneath it.
75
+ Footer: '@rxova/brand/components/Footer.astro',
76
+ ...components,
77
+ },
78
+
79
+ head: [
80
+ {
81
+ tag: 'meta' as const,
82
+ attrs: { property: 'og:image', content: `${RXOVA_ORIGIN}/og/${project}.png` },
83
+ },
84
+ {
85
+ tag: 'meta' as const,
86
+ attrs: { name: 'twitter:card', content: 'summary_large_image' },
87
+ },
88
+ ],
89
+
90
+ // Pagefind ships with Starlight and replaces the third-party search plugin
91
+ // journey was carrying.
92
+ pagefind: true,
93
+
94
+ sidebar,
95
+ }
96
+ }
package/src/tokens.css ADDED
@@ -0,0 +1,119 @@
1
+ /**
2
+ * rxova design tokens — the single source of truth for every rxova.org surface.
3
+ *
4
+ * Seeded from the original landing page (rxova-website/site/src/styles/global.css),
5
+ * which was the de-facto brand before this package existed.
6
+ *
7
+ * The palette is a warm monochrome: warm near-black ink on warm paper, with the
8
+ * logo's blue -> violet -> magenta gradient as the ONLY chroma in the system.
9
+ * Resist adding a second accent hue — the restraint is the identity.
10
+ *
11
+ * Every neutral flips between light and dark, so downstream mappings (see
12
+ * starlight.css) can reference a token once and get both modes for free.
13
+ */
14
+
15
+ :root,
16
+ :root[data-theme='light'] {
17
+ color-scheme: light;
18
+
19
+ /* Surfaces, ordered background -> foreground. */
20
+ --rx-bg: #ffffff;
21
+ --rx-card: #fbfaf7;
22
+ --rx-tag-bg: #f0ede6;
23
+ --rx-rule: #e7e3db;
24
+ --rx-rule-strong: #d5d0c6;
25
+
26
+ /* Text, ordered faintest -> strongest. */
27
+ --rx-faint: #8b867c;
28
+ --rx-muted: #605c54;
29
+ --rx-fg: #14140f;
30
+
31
+ /* Accent ramp: the gradient's violet mid-stop, for links and focus. */
32
+ --rx-primary: #6b3df0;
33
+ --rx-primary-dark: #5a2fd6;
34
+ --rx-primary-darker: #4a27b8;
35
+ --rx-primary-darkest: #3c1f96;
36
+ --rx-primary-light: #8159f3;
37
+ --rx-primary-lighter: #9375f5;
38
+ --rx-primary-lightest: #b7a3f9;
39
+
40
+ --rx-glow-tint: rgb(107 61 240 / 0.14);
41
+ --rx-shadow-soft: 0 20px 60px -30px rgb(107 61 240 / 0.35);
42
+ }
43
+
44
+ :root[data-theme='dark'] {
45
+ color-scheme: dark;
46
+
47
+ --rx-bg: #100f0d;
48
+ --rx-card: #17150f;
49
+ --rx-tag-bg: #201e18;
50
+ --rx-rule: #262420;
51
+ --rx-rule-strong: #3b3830;
52
+
53
+ --rx-faint: #7b766c;
54
+ --rx-muted: #a8a294;
55
+ --rx-fg: #f2efe8;
56
+
57
+ /* Lightened so the accent keeps AA contrast on a dark surface. */
58
+ --rx-primary: #9375f5;
59
+ --rx-primary-dark: #8159f3;
60
+ --rx-primary-darker: #6b3df0;
61
+ --rx-primary-darkest: #5a2fd6;
62
+ --rx-primary-light: #b7a3f9;
63
+ --rx-primary-lighter: #cbbcfb;
64
+ --rx-primary-lightest: #e0d7fd;
65
+
66
+ --rx-glow-tint: rgb(242 239 232 / 0.06);
67
+ --rx-shadow-soft: 0 20px 60px -30px rgb(0 0 0 / 0.6);
68
+ }
69
+
70
+ /* OS preference wins only when no explicit choice has been stored. */
71
+ @media (prefers-color-scheme: dark) {
72
+ :root:not([data-theme]) {
73
+ color-scheme: dark;
74
+
75
+ --rx-bg: #100f0d;
76
+ --rx-card: #17150f;
77
+ --rx-tag-bg: #201e18;
78
+ --rx-rule: #262420;
79
+ --rx-rule-strong: #3b3830;
80
+
81
+ --rx-faint: #7b766c;
82
+ --rx-muted: #a8a294;
83
+ --rx-fg: #f2efe8;
84
+
85
+ --rx-primary: #9375f5;
86
+ --rx-primary-dark: #8159f3;
87
+ --rx-primary-darker: #6b3df0;
88
+ --rx-primary-darkest: #5a2fd6;
89
+ --rx-primary-light: #b7a3f9;
90
+ --rx-primary-lighter: #cbbcfb;
91
+ --rx-primary-lightest: #e0d7fd;
92
+
93
+ --rx-glow-tint: rgb(242 239 232 / 0.06);
94
+ --rx-shadow-soft: 0 20px 60px -30px rgb(0 0 0 / 0.6);
95
+ }
96
+ }
97
+
98
+ /* Mode-independent tokens. */
99
+ :root {
100
+ /* The logo gradient. Use it for rules, hero accents and the switcher dot —
101
+ never for body text, where it cannot meet a contrast ratio. */
102
+ --rx-accent-a: #0a84ff;
103
+ --rx-accent-b: #6b3df0;
104
+ --rx-accent-c: #e6199b;
105
+ --rx-gradient: linear-gradient(90deg, var(--rx-accent-a), var(--rx-accent-b), var(--rx-accent-c));
106
+
107
+ --rx-font-sans:
108
+ 'Space Grotesk', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
109
+ --rx-font-mono:
110
+ 'IBM Plex Mono', ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
111
+
112
+ --rx-radius: 12px;
113
+ --rx-radius-sm: 6px;
114
+ --rx-radius-pill: 999px;
115
+
116
+ /* Prose measure for the landing; wider bound for marketing sections. */
117
+ --rx-max: 44rem;
118
+ --rx-max-wide: 72rem;
119
+ }